diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 4d4e605b9..e57365621 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -8,12 +8,19 @@ RUN apt-get update \ # Verify git and process tools are installed RUN apt-get install -y git procps -# Install yarn +# Install yarn, puppeteer deps RUN apt-get install -y curl apt-transport-https lsb-release \ && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ && apt-get update \ - && apt-get -y install --no-install-recommends yarn + && apt-get -y install --no-install-recommends \ + yarn fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ + # https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322921352 + gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ + libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ + libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ + libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ + ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget # Clean up RUN apt-get autoremove -y \ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..dccd0c5df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Question + url: https://stackoverflow.com/questions/tagged/xtermjs + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 1fe5c763a..000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Question -about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs ---- - -🛑 The issue tracker is not for questions 🛑 - -If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs. diff --git a/.gitignore b/.gitignore index 64273dda9..8aea04b2e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ lib/ out/ out-test/ +.nyc_output/ Makefile.gyp *.Makefile *.target.gyp.mk diff --git a/.vscode/settings.json b/.vscode/settings.json index 3c8a2dc2e..20e4915ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "typescript.preferences.quoteStyle": "single" + "typescript.preferences.quoteStyle": "single", + "typescript.preferences.importModuleSpecifier": "non-relative" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8102a2ee..d5efe9d27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,3 +52,7 @@ By contributing code to xterm.js you: holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct agreement with you.) + +### Test coverage + +One area that always needs attention is improving out unit test coverage, you can view the code coverage report on [Azure Pipelines](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) by clicking the Code Coverage tab. diff --git a/README.md b/README.md index 498278b89..d78dcc725 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b - **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer. - **Rich unicode support**: Supports CJK, emojis and IMEs. - **Self-contained**: Requires zero dependencies to work. -- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option. +- **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 @@ -110,7 +110,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2. - [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. - [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. -- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. +- [**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. @@ -154,6 +154,11 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components. - [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `` into your component. - [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet. +- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks. +- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal +- [**Gus**](https://gus.jp): A shared coding pad where you can run Python with xterm.js +- [**Linode**](https://linode.com): Linode uses xterm.js to provide users a web console for their Linode instances. +- [**FluffOS**](https://www.fluffos.info): Active maintained LPMUD driver with websocket support. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index 5a5c5d757..dd134308a 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-attach", - "version": "0.2.1", + "version": "0.4.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-attach/src/AttachAddon.api.ts b/addons/xterm-addon-attach/src/AttachAddon.api.ts index c5b2d8587..264b32ee1 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.api.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.api.ts @@ -20,8 +20,7 @@ describe('AttachAddon', () => { this.timeout(20000); browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); @@ -43,7 +42,7 @@ describe('AttachAddon', () => { const server = new WebSocket.Server({ port }); server.on('connection', socket => socket.send('foo')); await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo'); server.close(); }); @@ -55,7 +54,7 @@ describe('AttachAddon', () => { const data = new Uint8Array([102, 111, 111]); server.on('connection', socket => socket.send(data)); await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo'); server.close(); }); }); @@ -69,3 +68,12 @@ async function openTerminal(options: ITerminalOptions = {}): Promise { await page.waitForSelector('.xterm-text-layer'); } } + +async function pollFor(page: puppeteer.Page, fn: string, val: any): Promise { + const result = await page.evaluate(fn); + if (result !== val) { + return new Promise(r => { + setTimeout(() => r(pollFor(page, fn, val)), 10); + }); + } +} diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 117b2b584..279d1b2ee 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -33,6 +33,7 @@ export class AttachAddon implements ITerminalAddon { if (this._bidirectional) { this._disposables.push(terminal.onData(data => this._sendData(data))); + this._disposables.push(terminal.onBinary(data => this._sendBinary(data))); } this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); @@ -51,6 +52,17 @@ export class AttachAddon implements ITerminalAddon { } this._socket.send(data); } + + private _sendBinary(data: string): void { + if (this._socket.readyState !== 1) { + return; + } + const buffer = new Uint8Array(data.length); + for (let i = 0; i < data.length; ++i) { + buffer[i] = data.charCodeAt(i) & 255; + } + this._socket.send(buffer); + } } function addSocketListener(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable { diff --git a/addons/xterm-addon-attach/src/tsconfig.json b/addons/xterm-addon-attach/src/tsconfig.json index d875aa530..5539aa564 100644 --- a/addons/xterm-addon-attach/src/tsconfig.json +++ b/addons/xterm-addon-attach/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es5", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index fa1bf8528..4bceb258e 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.2.1", + "version": "0.3.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-fit/src/FitAddon.api.ts b/addons/xterm-addon-fit/src/FitAddon.api.ts new file mode 100644 index 000000000..aa73a9b61 --- /dev/null +++ b/addons/xterm-addon-fit/src/FitAddon.api.ts @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 1024; +const height = 768; + +describe('FitAddon', () => { + before(async function(): Promise { + this.timeout(20000); + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + args: [`--window-size=${width},${height}`, `--no-sandbox`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + await page.goto(APP); + await openTerminal(); + }); + + after(async () => { + await browser.close(); + }); + + it('no terminal', async function(): Promise { + await page.evaluate(`window.fit = new FitAddon();`); + assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined); + }); + + describe('proposeDimensions', () => { + afterEach(async () => { + return unloadFit(); + }); + + it('default', async function(): Promise { + await loadFit(); + assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), { + cols: 87, + rows: 26 + }); + }); + + it('width', async function(): Promise { + await loadFit(1008); + assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), { + cols: 110, + rows: 26 + }); + }); + + it('small', async function(): Promise { + await loadFit(1, 1); + assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), { + cols: 2, + rows: 1 + }); + }); + }); + + describe('fit', () => { + afterEach(async () => { + return unloadFit(); + }); + + it('default', async function(): Promise { + await loadFit(); + await page.evaluate(`window.fit.fit()`); + assert.equal(await page.evaluate(`window.term.cols`), 87); + assert.equal(await page.evaluate(`window.term.rows`), 26); + }); + + it('width', async function(): Promise { + await loadFit(1008); + await page.evaluate(`window.fit.fit()`); + assert.equal(await page.evaluate(`window.term.cols`), 110); + assert.equal(await page.evaluate(`window.term.rows`), 26); + }); + + it('small', async function(): Promise { + await loadFit(1, 1); + await page.evaluate(`window.fit.fit()`); + assert.equal(await page.evaluate(`window.term.cols`), 2); + assert.equal(await page.evaluate(`window.term.rows`), 1); + }); + }); +}); + +async function loadFit(width: number = 800, height: number = 450): Promise { + await page.evaluate(` + window.fit = new FitAddon(); + window.term.loadAddon(window.fit); + document.querySelector('#terminal-container').style.width='${width}px'; + document.querySelector('#terminal-container').style.height='${height}px'; + `); +} + +async function unloadFit(): Promise { + await page.evaluate(`window.fit.dispose();`); +} + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} diff --git a/addons/xterm-addon-fit/src/FitAddon.ts b/addons/xterm-addon-fit/src/FitAddon.ts index f23bd161d..ca7e24b16 100644 --- a/addons/xterm-addon-fit/src/FitAddon.ts +++ b/addons/xterm-addon-fit/src/FitAddon.ts @@ -17,6 +17,9 @@ interface ITerminalDimensions { cols: number; } +const MINIMUM_COLS = 2; +const MINIMUM_ROWS = 1; + export class FitAddon implements ITerminalAddon { private _terminal: Terminal | undefined; @@ -49,7 +52,7 @@ export class FitAddon implements ITerminalAddon { return undefined; } - if (!this._terminal.element.parentElement) { + if (!this._terminal.element || !this._terminal.element.parentElement) { return undefined; } @@ -71,8 +74,8 @@ export class FitAddon implements ITerminalAddon { const availableHeight = parentElementHeight - elementPaddingVer; const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth; const geometry = { - cols: Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth), - rows: Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight) + cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)), + rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight)) }; return geometry; } diff --git a/addons/xterm-addon-search/fixtures/issue-2444 b/addons/xterm-addon-search/fixtures/issue-2444 new file mode 100644 index 000000000..cb7f78783 --- /dev/null +++ b/addons/xterm-addon-search/fixtures/issue-2444 @@ -0,0 +1,70 @@ +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +make[3]: Nothing to be done for `libsrc/wrap_curl/CMakeFiles/wrap_curl.dir/build'. +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +[ 0%] /usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +[ 0%] [ 0%] /usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +Built target config_utils +[ 0%] make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +Built target omega +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +cd /home/yan/ssd/searchtrunk/tmp/release && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/yan/ssd/searchtrunk /home/yan/ssd/searchtrunk/projects/protogen/cpp /home/yan/ssd/searchtrunk/tmp/release /home/yan/ssd/searchtrunk/tmp/release/projects/protogen/cpp /home/yan/ssd/searchtrunk/tmp/release/projects/protogen/cpp/CMakeFiles/protogen.dir/DependInfo.cmake --color= +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +Built target morpheus +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +make -f libsrc/base/CMakeFiles/base.dir/build.make libsrc/base/CMakeFiles/base.dir/build +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +make[3]: Nothing to be done for `libsrc/coroutine/CMakeFiles/coroutine.dir/build'. +Built target wrap_curl +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +[ 0%] [ 0%] [ 0%] [ 0%] Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/crop_context.cpp.o +cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/crop_context.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/crop_context.cpp +Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/detector.cpp.o +Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/image_crop.cpp.o +cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/detector.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/detector.cpp +Built target coroutine +cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/image_crop.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/image_crop.cpp +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +make[3]: Nothing to be done for `libsrc/base/CMakeFiles/base.dir/build'. +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +make -f projects/protogen/cpp/CMakeFiles/protogen.dir/build.make projects/protogen/cpp/CMakeFiles/protogen.dir/build +[ 0%] Built target base +In file included from /home/yan/ssd/searchtrunk/include/image_crop/detector.hpp:3:0, + from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/detector.cpp:2: +/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory + #include + ^~~~~~~~~~~~~~~~~ +compilation terminated. +make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/detector.cpp.o] Error 1 +make[3]: *** Waiting for unfinished jobs.... +In file included from /home/yan/ssd/searchtrunk/include/image_crop/image_crop.hpp:3:0, + from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/image_crop.cpp:1: +/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory + #include + ^~~~~~~~~~~~~~~~~ +compilation terminated. +make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/image_crop.cpp.o] Error 1 +In file included from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/crop_context.cpp:1:0: +/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory + #include + ^~~~~~~~~~~~~~~~~ +compilation terminated. +make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/crop_context.cpp.o] Error 1 +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +make[2]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/all] Error 2 +make[2]: *** Waiting for unfinished jobs.... +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +make[3]: Nothing to be done for `projects/protogen/cpp/CMakeFiles/protogen.dir/build'. +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles 63 64 65 66 +[ 25%] Built target protogen +make[2]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +make[1]: *** [all] Error 2 +make[1]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release/projects/projectX' +make: *** [install] Error 2 +yan@yPC:~/ssd/searchtrunk/projects/projectX$ diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index c2e14032b..d66e8a687 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.2.1", + "version": "0.4.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-search/src/SearchAddon.api.ts b/addons/xterm-addon-search/src/SearchAddon.api.ts index 14e12a8c7..72a69b5fb 100644 --- a/addons/xterm-addon-search/src/SearchAddon.api.ts +++ b/addons/xterm-addon-search/src/SearchAddon.api.ts @@ -6,6 +6,8 @@ import * as puppeteer from 'puppeteer'; import { assert } from 'chai'; import { ITerminalOptions } from 'xterm'; +import { readFile } from 'fs'; +import { resolve } from 'path'; const APP = 'http://127.0.0.1:3000/test'; @@ -15,13 +17,12 @@ const width = 800; const height = 600; describe('Search Tests', function (): void { - this.timeout(200000); + this.timeout(20000); before(async function (): Promise { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); @@ -91,13 +92,101 @@ describe('Search Tests', function (): void { selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc'); }); - it ('Simple Regex', async () => { + it('Simple Regex', async () => { await writeSync('abc123defABCD'); await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`); assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc'); await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`); assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD'); }); + + it('Search for single result twice should not unselect it', async () => { + await writeSync('abc def'); + assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true); + assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc'); + assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true); + assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc'); + }); + + describe('Regression tests', () => { + describe('#2444 wrapped line content not being found', () => { + let fixture: string; + before(async () => { + const rawFixture = await new Promise(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data))); + fixture = rawFixture.toString() + .replace(/\n/g, '\\n\\r') + .replace(/'/g, '\\\''); + }); + it('should find all occurrences using findNext', async () => { + await writeSync(fixture); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + // Wrap around to first result + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + }); + it('should find all occurrences using findPrevious', async () => { + await writeSync(fixture); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + // Wrap around to first result + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + }); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 4d3e8841e..db2968e69 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, IDisposable, ITerminalAddon } from 'xterm'; +import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm'; export interface ISearchOptions { regex?: boolean; @@ -12,6 +12,11 @@ export interface ISearchOptions { incremental?: boolean; } +export interface ISearchPosition { + startCol: number; + startRow: number; +} + export interface ISearchResult { term: string; col: number; @@ -59,29 +64,35 @@ export class SearchAddon implements ITerminalAddon { let startCol = 0; let startRow = 0; - + let currentSelection: ISelectionPosition | undefined; if (this._terminal.hasSelection()) { const incremental = searchOptions ? searchOptions.incremental : false; // Start from the selection end if there is a selection // For incremental search, use existing row - const currentSelection = this._terminal.getSelectionPosition()!; + currentSelection = this._terminal.getSelectionPosition()!; startRow = incremental ? currentSelection.startRow : currentSelection.endRow; startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; } this._initLinesCache(); + const searchPosition: ISearchPosition = { + startRow, + startCol + }; + // Search startRow - let result = this._findInLine(term, startRow, startCol, searchOptions); + let result = this._findInLine(term, searchPosition, searchOptions); // Search from startRow + 1 to end if (!result) { for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) { - + searchPosition.startRow = y; + searchPosition.startCol = 0; // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index - result = this._findInLine(term, y, 0, searchOptions); + result = this._findInLine(term, searchPosition, searchOptions); if (result) { break; } @@ -90,13 +101,18 @@ export class SearchAddon implements ITerminalAddon { // If we hit the bottom and didn't search from the very top wrap back up if (!result && startRow !== 0) { for (let y = 0; y < startRow; y++) { - result = this._findInLine(term, y, 0, searchOptions); + searchPosition.startRow = y; + searchPosition.startCol = 0; + result = this._findInLine(term, searchPosition, searchOptions); if (result) { break; } } } + // If there is only one result, return true. + if (!result && currentSelection) return true; + // Set selection and scroll if a result was found return this._selectResult(result); } @@ -121,31 +137,37 @@ export class SearchAddon implements ITerminalAddon { const isReverseSearch = true; let startRow = this._terminal.buffer.baseY + this._terminal.rows; let startCol = this._terminal.cols; - let result: ISearchResult | undefined = undefined; + let result: ISearchResult | undefined; const incremental = searchOptions ? searchOptions.incremental : false; + let currentSelection: ISelectionPosition | undefined; if (this._terminal.hasSelection()) { - const currentSelection = this._terminal.getSelectionPosition()!; + currentSelection = this._terminal.getSelectionPosition()!; // Start from selection start if there is a selection startRow = currentSelection.startRow; startCol = currentSelection.startColumn; } this._initLinesCache(); + const searchPosition: ISearchPosition = { + startRow, + startCol + }; if (incremental) { - result = this._findInLine(term, startRow, startCol, searchOptions, false); + result = this._findInLine(term, searchPosition, searchOptions, false); if (!(result && result.row === startRow && result.col === startCol)) { - result = this._findInLine(term, startRow, startCol, searchOptions, true); + result = this._findInLine(term, searchPosition, searchOptions, true); } } else { - result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch); + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); } // Search from startRow - 1 to top if (!result) { - startCol = this._terminal.cols; + searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols); for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch); + searchPosition.startRow = y; + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); if (result) { break; } @@ -154,13 +176,17 @@ export class SearchAddon implements ITerminalAddon { // If we hit the top and didn't search from the very bottom wrap back down if (!result && startRow !== (this._terminal.buffer.baseY + this._terminal.rows)) { for (let y = (this._terminal.buffer.baseY + this._terminal.rows); y > startRow; y--) { - result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch); + searchPosition.startRow = y; + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); if (result) { break; } } } + // If there is only one result, return true. + if (!result && currentSelection) return true; + // Set selection and scroll if a result was found return this._selectResult(result); } @@ -213,18 +239,28 @@ export class SearchAddon implements ITerminalAddon { * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the * text starts on is searched. * @param term The search term. - * @param row The line to start the search from. - * @param col The column to start the search from. + * @param position The position to start the search. * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { + protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { const terminal = this._terminal!; + let row = searchPosition.startRow; + const col = searchPosition.startCol; // Ignore wrapped lines, only consider on unwrapped line (first row of command string). const firstLine = terminal.buffer.getLine(row); if (firstLine && firstLine.isWrapped) { - return; + if (isReverseSearch) { + searchPosition.startCol += terminal.cols; + return; + } + + // This will iterate until we find the line start. + // When we find it, we will search using the calculated start column. + searchPosition.startRow--; + searchPosition.startCol += terminal.cols; + return this._findInLine(term, searchPosition, searchOptions); } let stringLine = this._linesCache ? this._linesCache[row] : void 0; if (stringLine === void 0) { @@ -344,7 +380,7 @@ export class SearchAddon implements ITerminalAddon { } terminal.select(result.col, result.row, result.term.length); // If it is not in the viewport then we scroll else it just gets selected - if (result.row > (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) { + if (result.row >= (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) { let scroll = result.row - terminal.buffer.viewportY; scroll = scroll - Math.floor(terminal.rows / 2); terminal.scrollLines(scroll); diff --git a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts index 94bc52971..f27aba783 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -17,7 +17,7 @@ declare module 'xterm-addon-search' { /** * Whether to search for a whole word, the result is only valid if it's - * suppounded in "non-word" characters such as `_`, `(`, `)` or space. + * surrounded in "non-word" characters such as `_`, `(`, `)` or space. */ wholeWord?: boolean; @@ -27,7 +27,7 @@ declare module 'xterm-addon-search' { caseSensitive?: boolean; /** - * Whether to do an indcremental search, this will expand the selection if it + * Whether to do an incremental search, this will expand the selection if it * still matches the term the user typed. Note that this only affects * `findNext`, not `findPrevious`. */ diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts index 2c0044bef..ab61fdbfc 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts @@ -19,8 +19,7 @@ describe('WebLinksAddon', () => { this.timeout(10000); browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); @@ -54,22 +53,21 @@ describe('WebLinksAddon', () => { async function testHostName(hostname: string): Promise { await openTerminal({ rendererType: 'dom' }); await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`); - await page.evaluate(` - window.term.writeln(' http://${hostname} '); - window.term.writeln(' http://${hostname}/a~b#c~d?e~f '); - window.term.writeln(' http://${hostname}/colon:test '); - window.term.writeln(' http://${hostname}/colon:test: '); - window.term.writeln('"http://${hostname}/"'); - window.term.writeln('\\'http://${hostname}/\\''); - window.term.writeln('http://${hostname}/subpath/+/id'); - `); - assert.equal(await getLinkAtCell(3, 1), `http://${hostname}`); - assert.equal(await getLinkAtCell(3, 2), `http://${hostname}/a~b#c~d?e~f`); - assert.equal(await getLinkAtCell(3, 3), `http://${hostname}/colon:test`); - assert.equal(await getLinkAtCell(3, 4), `http://${hostname}/colon:test`); - assert.equal(await getLinkAtCell(2, 5), `http://${hostname}/`); - assert.equal(await getLinkAtCell(2, 6), `http://${hostname}/`); - assert.equal(await getLinkAtCell(1, 7), `http://${hostname}/subpath/+/id`); + const data = ` http://${hostname} \\r\\n` + + ` http://${hostname}/a~b#c~d?e~f \\r\\n` + + ` http://${hostname}/colon:test \\r\\n` + + ` http://${hostname}/colon:test: \\r\\n` + + `"http://${hostname}/"\\r\\n` + + `\\'http://${hostname}/\\'\\r\\n` + + `http://${hostname}/subpath/+/id`; + await writeSync(page, data); + await pollForLinkAtCell(3, 1, `http://${hostname}`); + await pollForLinkAtCell(3, 2, `http://${hostname}/a~b#c~d?e~f`); + await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`); + await pollForLinkAtCell(3, 4, `http://${hostname}/colon:test`); + await pollForLinkAtCell(2, 5, `http://${hostname}/`); + await pollForLinkAtCell(2, 6, `http://${hostname}/`); + await pollForLinkAtCell(1, 7, `http://${hostname}/subpath/+/id`); } async function openTerminal(options: ITerminalOptions = {}): Promise { @@ -82,8 +80,30 @@ async function openTerminal(options: ITerminalOptions = {}): Promise { } } -async function getLinkAtCell(col: number, row: number): Promise { +async function pollForLinkAtCell(col: number, row: number, value: string): Promise { const rowSelector = `.xterm-rows > :nth-child(${row})`; - await page.hover(`${rowSelector} > :nth-child(${col})`); - return await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`); + // Ensure the hover element exists before trying to hover it + await pollFor(page, `!!document.querySelector('${rowSelector} > :nth-child(${col})')`, true); + await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`)); + assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value); +} + +async function pollFor(page: puppeteer.Page, fn: string, val: any, preFn?: () => Promise): Promise { + if (preFn) { + await preFn(); + } + const result = await page.evaluate(fn); + if (result !== val) { + return new Promise(r => { + setTimeout(() => r(pollFor(page, fn, val, preFn)), 10); + }); + } +} + +async function writeSync(page: puppeteer.Page, data: string): Promise { + await page.evaluate(` + window.ready = false; + window.term.write('${data}', () => window.ready = true); + `); + await pollFor(page, 'window.ready', true); } diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index 9a01aea05..550635250 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.2.1", + "version": "0.4.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-webgl/src/CharDataCompat.ts b/addons/xterm-addon-webgl/src/CharDataCompat.ts deleted file mode 100644 index 13613c884..000000000 --- a/addons/xterm-addon-webgl/src/CharDataCompat.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { CellData } from 'common/buffer/CellData'; -import { FLAGS } from './Constants'; -import { IBufferLine } from 'common/Types'; - -export function getCompatAttr(bufferLine: IBufferLine, index: number): number { - // TODO: Need to move WebGL over to the new system and remove this block - const cell = new CellData(); - bufferLine.loadCell(index, cell); - const oldBg = cell.getBgColor() === -1 ? 256 : cell.getBgColor(); - const oldFg = cell.getFgColor() === -1 ? 256 : cell.getFgColor(); - const oldAttr = - (cell.isBold() ? FLAGS.BOLD : 0) | - (cell.isUnderline() ? FLAGS.UNDERLINE : 0) | - (cell.isBlink() ? FLAGS.BLINK : 0) | - (cell.isInverse() ? FLAGS.INVERSE : 0) | - (cell.isDim() ? FLAGS.DIM : 0) | - (cell.isItalic() ? FLAGS.ITALIC : 0); - const attrCompat = - oldBg | - (oldFg << 9) | - (oldAttr << 18); - return attrCompat; -} diff --git a/addons/xterm-addon-webgl/src/ColorUtils.ts b/addons/xterm-addon-webgl/src/ColorUtils.ts deleted file mode 100644 index 80372e650..000000000 --- a/addons/xterm-addon-webgl/src/ColorUtils.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @license MIT - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - */ - -import { IColor } from 'browser/Types'; - -export function getLuminance(color: IColor): number { - // Coefficients taken from: https://www.w3.org/TR/AERT/#color-contrast - const r = color.rgba >> 24 & 0xff; - const g = color.rgba >> 16 & 0xff; - const b = color.rgba >> 8 & 0xff; - return (0.299 * r + 0.587 * g + 0.114 * b) / 255; -} diff --git a/addons/xterm-addon-webgl/src/Constants.ts b/addons/xterm-addon-webgl/src/Constants.ts deleted file mode 100644 index ab18b97ab..000000000 --- a/addons/xterm-addon-webgl/src/Constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -// TODO: Should be removed after chardata workaround is fixed -export const enum FLAGS { - BOLD = 1, - UNDERLINE = 2, - BLINK = 4, - INVERSE = 8, - INVISIBLE = 16, - DIM = 32, - ITALIC = 64 -} diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 186cb2c1b..332abeb8f 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -6,15 +6,14 @@ import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; -import { INDICIES_PER_CELL } from './WebglRenderer'; -import { COMBINED_CHAR_BIT_MASK } from './RenderModel'; +import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel'; import { fill } from 'common/TypedArrayUtils'; import { slice } from './TypedArray'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; -import { getLuminance } from './ColorUtils'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal, IBufferLine } from 'xterm'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IColor } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; +import { AttributeData } from 'common/buffer/AttributeData'; interface IVertices { attributes: Float32Array; @@ -103,11 +102,7 @@ export class GlyphRenderer { private _dimensions: IRenderDimensions ) { const gl = this._gl; - const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); - if (program === undefined) { - throw new Error('Could not create WebGL program'); - } this._program = program; // Uniform locations @@ -172,11 +167,11 @@ export class GlyphRenderer { return this._atlas ? this._atlas.beginFrame() : true; } - public updateCell(x: number, y: number, code: number, attr: number, bg: number, fg: number, chars: string): void { - this._updateCell(this._vertices.attributes, x, y, code, attr, bg, fg, chars); + public updateCell(x: number, y: number, code: number, bg: number, fg: number, chars: string): void { + this._updateCell(this._vertices.attributes, x, y, code, bg, fg, chars); } - private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, attr: number, bg: number, fg: number, chars?: string): void { + private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, chars?: string): void { const terminal = this._terminal; const i = (y * terminal.cols + x) * INDICES_PER_CELL; @@ -189,12 +184,12 @@ export class GlyphRenderer { let rasterizedGlyph: IRasterizedGlyph; if (!this._atlas) { - throw new Error('atlas must be set before updating cell'); + return; } if (chars && chars.length > 1) { - rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, attr, bg, fg); + rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg); } else { - rasterizedGlyph = this._atlas.getRasterizedGlyph(code, attr, bg, fg); + rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg); } // Fill empty if no glyph was found @@ -223,62 +218,86 @@ export class GlyphRenderer { this._vertices.selectionAttributes = slice(this._vertices.attributes, 0); - // TODO: Make fg and bg configurable, currently since the buffer doesn't - // support truecolor the char atlas cannot store it. - const lumi = getLuminance(this._colors.background); - const fg = lumi > 0.5 ? 7 : 0; - const bg = lumi > 0.5 ? 0 : 7; + const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB; if (columnSelectMode) { const startCol = model.selection.startCol; const width = model.selection.endCol - startCol; const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1; for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) { - this._updateSelectionRange(startCol, startCol + width, y, model, bg, fg); + this._updateSelectionRange(startCol, startCol + width, y, model, bg); } } else { // Draw first row const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0; const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; - this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg, fg); + this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg); // Draw middle rows const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0); for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) { - this._updateSelectionRange(0, startRowEndCol, y, model, bg, fg); + this._updateSelectionRange(0, startRowEndCol, y, model, bg); } // Draw final row if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) { // Only draw viewportEndRow if it's not the same as viewportStartRow const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; - this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg, fg); + this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg); } } } - private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number, fg: number): void { + private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void { const terminal = this._terminal; const row = y + terminal.buffer.viewportY; let line: IBufferLine | undefined; for (let x = startCol; x < endCol; x++) { - const offset = (y * this._terminal.cols + x) * INDICIES_PER_CELL; - // Because the cache uses attr as a lookup key it needs to contain the selection colors as well - let attr = model.cells[offset + 1]; - attr = attr & ~0x3ffff | bg << 9 | fg; + const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL; const code = model.cells[offset]; + let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET]; + if (fg & FgFlags.INVERSE) { + const workCell = new AttributeData(); + workCell.fg = fg; + workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET]; + // 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)); + switch (workCell.getBgColorMode()) { + case Attributes.CM_P16: + case Attributes.CM_P256: + const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba; + fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK; + case Attributes.CM_RGB: + const arr = AttributeData.toColorRGB(workCell.getBgColor()); + fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT; + case Attributes.CM_DEFAULT: + default: + const c2 = this._colors.background.rgba; + fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK; + } + fg |= Attributes.CM_RGB; + } if (code & COMBINED_CHAR_BIT_MASK) { if (!line) { line = terminal.buffer.getLine(row); } const chars = line!.getCell(x)!.char; - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg, chars); + this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars); } else { - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg); + this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg); } } } + private _getColorFromAnsiIndex(idx: number): IColor { + if (idx >= this._colors.ansi.length) { + throw new Error('No color found for idx ' + idx); + } + return this._colors.ansi[idx]; + } + public onResize(): void { const terminal = this._terminal; const gl = this._gl; diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 2c464d2ae..2246cddd9 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -6,12 +6,11 @@ import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types'; import { fill } from 'common/TypedArrayUtils'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { is256Color } from './atlas/CharAtlasUtils'; -import { DEFAULT_COLOR } from 'common/buffer/Constants'; +import { Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; import { IColorSet, IColor } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; +import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; const enum VertexAttribLocations { POSITION = 0, @@ -23,13 +22,13 @@ const enum VertexAttribLocations { const vertexShaderSource = `#version 300 es layout (location = ${VertexAttribLocations.POSITION}) in vec2 a_position; layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size; -layout (location = ${VertexAttribLocations.COLOR}) in vec3 a_color; +layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color; layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad; uniform mat4 u_projection; uniform vec2 u_resolution; -out vec3 v_color; +out vec4 v_color; void main() { vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution; @@ -40,12 +39,12 @@ void main() { const fragmentShaderSource = `#version 300 es precision lowp float; -in vec3 v_color; +in vec4 v_color; out vec4 outColor; void main() { - outColor = vec4(v_color, 1); + outColor = v_color; }`; interface IVertices { @@ -156,7 +155,7 @@ export class RectangleRenderer { private _updateCachedColors(): void { this._bgFloat = this._colorToFloat32Array(this._colors.background); - this._selectionFloat = this._colorToFloat32Array(this._colors.selection); + this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque); } private _updateViewportRectangle(): void { @@ -247,47 +246,73 @@ export class RectangleRenderer { for (let y = 0; y < terminal.rows; y++) { let currentStartX = -1; - let currentBg = DEFAULT_COLOR; + let currentBg = 0; + let currentFg = 0; + let currentInverse = false; for (let x = 0; x < terminal.cols; x++) { - const modelIndex = ((y * terminal.cols) + x) * 4; - const bg = model.cells[modelIndex + 2]; - if (bg !== currentBg) { + const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; + const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; + const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; + const inverse = !!(fg & FgFlags.INVERSE); + if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) { // A rectangle needs to be drawn if going from non-default to another color - if (currentBg !== DEFAULT_COLOR) { + if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; - this._updateRectangle(vertices, offset, currentBg, currentStartX, x, y); + this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y); } currentStartX = x; currentBg = bg; + currentFg = fg; + currentInverse = inverse; } } // Finish rectangle if it's still going - if (currentBg !== DEFAULT_COLOR) { + if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; - this._updateRectangle(vertices, offset, currentBg, currentStartX, terminal.cols, y); + this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y); } } vertices.count = rectangleCount; } - private _updateRectangle(vertices: IVertices, offset: number, bg: number, startX: number, endX: number, y: number): void { - let color: IColor | null = null; - if (bg === INVERTED_DEFAULT_COLOR) { - color = this._colors.foreground; - } else if (is256Color(bg)) { - color = this._colors.ansi[bg]; + private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { + let rgba: number | undefined; + if (fg & FgFlags.INVERSE) { + switch (fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + rgba = (fg & Attributes.RGB_MASK) << 8; + break; + case Attributes.CM_DEFAULT: + default: + rgba = this._colors.foreground.rgba; + } } else { - // TODO: Add support for true color - color = this._colors.foreground; + switch (bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + rgba = (bg & Attributes.RGB_MASK) << 8; + break; + case Attributes.CM_DEFAULT: + default: + rgba = this._colors.background.rgba; + } } + if (vertices.attributes.length < offset + 4) { vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE); } const x1 = startX * this._dimensions.scaledCellWidth; const y1 = y * this._dimensions.scaledCellHeight; - const r = ((color.rgba >> 24) & 0xFF) / 255; - const g = ((color.rgba >> 16) & 0xFF) / 255; - const b = ((color.rgba >> 8 ) & 0xFF) / 255; + const r = ((rgba >> 24) & 0xFF) / 255; + const g = ((rgba >> 16) & 0xFF) / 255; + const b = ((rgba >> 8 ) & 0xFF) / 255; this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1); } diff --git a/addons/xterm-addon-webgl/src/RenderModel.ts b/addons/xterm-addon-webgl/src/RenderModel.ts index a9ee24e9a..2b48047f8 100644 --- a/addons/xterm-addon-webgl/src/RenderModel.ts +++ b/addons/xterm-addon-webgl/src/RenderModel.ts @@ -6,7 +6,9 @@ import { IRenderModel, ISelectionRenderModel } from './Types'; import { fill } from 'common/TypedArrayUtils'; -export const RENDER_MODEL_INDICIES_PER_CELL = 4; +export const RENDER_MODEL_INDICIES_PER_CELL = 3; +export const RENDER_MODEL_BG_OFFSET = 1; +export const RENDER_MODEL_FG_OFFSET = 2; export const COMBINED_CHAR_BIT_MASK = 0x80000000; diff --git a/addons/xterm-addon-webgl/src/Types.d.ts b/addons/xterm-addon-webgl/src/Types.d.ts index 6ebc8d64d..f01092eec 100644 --- a/addons/xterm-addon-webgl/src/Types.d.ts +++ b/addons/xterm-addon-webgl/src/Types.d.ts @@ -4,7 +4,7 @@ */ export interface IRasterizedGlyphSet { - [flags: number]: IRasterizedGlyph; + [bg: number]: { [fg: number]: IRasterizedGlyph } | undefined; } /** diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 0074f9b28..3cf45f352 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -9,20 +9,35 @@ import { IRenderService } from 'browser/services/Services'; import { IColorSet } from 'browser/Types'; export class WebglAddon implements ITerminalAddon { + private _terminal?: Terminal; + private _renderer?: WebglRenderer; + constructor( private _preserveDrawingBuffer?: boolean ) {} public activate(terminal: Terminal): void { if (!terminal.element) { - throw new Error('Cannot activate WebglRendererAddon before Terminal.open'); + 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; - renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer)); + this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer); + renderService.setRenderer(this._renderer); } public dispose(): void { - throw new Error('WebglRendererAddon.dispose Not yet implemented'); + if (!this._terminal) { + throw new Error('Cannot dispose WebglAddon because it is activated'); + } + const renderService: IRenderService = (this._terminal)._core._renderService; + renderService.setRenderer((this._terminal)._core._createRenderer()); + renderService.onResize(this._terminal.cols, this._terminal.rows); + this._renderer = undefined; + } + + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderer?.textureAtlas; } } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 66be22d93..8b9c84bf7 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -4,9 +4,10 @@ */ import * as puppeteer from 'puppeteer'; -import { assert } from 'chai'; import { ITerminalOptions } from '../../../src/Types'; import { ITheme } from 'xterm'; +import { assert } from 'chai'; +import deepEqual = require('deep-equal'); const APP = 'http://127.0.0.1:3000/test'; @@ -16,31 +17,20 @@ const width = 800; const height = 600; describe('WebGL Renderer Integration Tests', function(): void { - this.timeout(20000); - - before(async function(): Promise { - browser = await puppeteer.launch({ - headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] - }); - page = (await browser.pages())[0]; - await page.setViewport({ width, height }); - await page.goto(APP); - await openTerminal(); - await page.evaluate(`window.term.loadAddon(new WebglAddon(true));`); + it('dispose removes renderer canvases', async () => { + await setupBrowser(); + assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3); + await page.evaluate(`addon.dispose()`); + assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); + await browser.close(); }); - after(() => { - browser.close(); - }); + describe('colors', () => { + before(async () => setupBrowser()); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); - beforeEach(async () => { - await page.evaluate(`window.term.reset()`); - }); - - describe('WebGL Renderer', () => { - it('foreground colors normal', async function(): Promise { + it('foreground 0-15', async () => { const theme: ITheme = { black: '#010203', red: '#040506', @@ -53,17 +43,17 @@ describe('WebGL Renderer Integration Tests', function(): void { }; await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); await writeSync(`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`); - assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]); - assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]); - assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]); - assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]); - assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]); - assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]); - assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]); - assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); - it('foreground colors bright', async function(): Promise { + it('foreground 0-7 drawBoldTextInBrightColors', async () => { const theme: ITheme = { brightBlack: '#010203', brightRed: '#040506', @@ -74,19 +64,22 @@ describe('WebGL Renderer Integration Tests', function(): void { brightCyan: '#131415', brightWhite: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); - await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`); - assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]); - assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]); - assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]); - assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]); - assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]); - assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]); - assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]); - assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]); + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('drawBoldTextInBrightColors', true); + `); + await writeSync(`\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); - it('background colors normal', async function(): Promise { + it('background 0-15', async () => { const theme: ITheme = { black: '#010203', red: '#040506', @@ -99,17 +92,132 @@ describe('WebGL Renderer Integration Tests', function(): void { }; await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); await writeSync(`\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `); - assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]); - assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]); - assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]); - assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]); - assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]); - assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]); - assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]); - assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); - it('background colors bright', async function(): Promise { + it('foreground 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + it('background 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + it('foreground 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[8;30m \\x1b[8;31m \\x1b[8;32m \\x1b[8;33m \\x1b[8;34m \\x1b[8;35m \\x1b[8;36m \\x1b[8;37m `); + await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(2, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(4, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(5, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(6, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(7, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(8, 1), [0, 0, 0, 255]); + }); + + it('background 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[8;40m█\\x1b[8;41m█\\x1b[8;42m█\\x1b[8;43m█\\x1b[8;44m█\\x1b[8;45m█\\x1b[8;46m█\\x1b[8;47m█`); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + it('foreground 0-15 bright', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + it('background 0-15 bright', async () => { const theme: ITheme = { brightBlack: '#010203', brightRed: '#040506', @@ -122,14 +230,638 @@ describe('WebGL Renderer Integration Tests', function(): void { }; await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); await writeSync(`\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `); - assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]); - assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]); - assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]); - assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]); - assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]); - assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]); - assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]); - assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + it('foreground 16-255', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + it('background 16-255', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + it('foreground 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + it('background 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + it('foreground 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + it('background 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + it('foreground true color red', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[38;2;${i};0;0m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + it('background true color red', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[48;2;${i};0;0m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + it('foreground true color green', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[38;2;0;${i};0m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + it('background true color green', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[48;2;0;${i};0m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + it('foreground true color blue', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[38;2;0;0;${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + it('background true color blue', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[48;2;0;0;${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + it('foreground true color grey', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[38;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + it('background true color grey', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[48;2;${i};${i};${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + it('foreground true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + it('background true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;${i};0;0m█\\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + it('foreground true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;0;${i};0m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + it('background true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;0;${i};0m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + it('foreground true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;0;0;${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + it('background true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;0;0;${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + it('foreground true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + it('background true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + it('foreground true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[8;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + it('background true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + }); + + describe('minimumContrastRatio', async () => { + before(async () => setupBrowser()); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + + it('should adjust 0-15 colors on black background', async () => { + const theme: ITheme = { + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('minimumContrastRatio', 1); + `); + await writeSync( + `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` + + `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█` + ); + // Validate before minimumContrastRatio is applied + await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are note + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`); + await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]); + await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]); + await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]); + await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]); + await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]); + await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]); + await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]); + await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]); + await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]); + await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]); + await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]); + await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]); + await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]); + await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]); + // Unchanged + await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + }); + + it('should adjust 0-15 colors on white background', async () => { + const theme: ITheme = { + background: '#ffffff', + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('minimumContrastRatio', 1); + `); + await writeSync( + `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` + + `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█` + ); + // Validate before minimumContrastRatio is applied + await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are note + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`); + await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]); + await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]); + await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]); + await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]); + await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]); + await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]); + await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]); + await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]); + await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]); + await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]); + await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]); + await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]); + await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]); + await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]); + await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); + }); + }); + + describe('selection', async () => { + before(async () => setupBrowser()); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + + it('should resolve the inverse foreground color based on the original background color, not the selection', async () => { + const theme: ITheme = { + foreground: '#FF0000', + background: '#00FF00', + selection: '#0000FF' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(` █\\x1b[7m█\\x1b[0m`); + await pollFor(page, () => getCellColor(1, 1), [0, 255, 0, 255]); + await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]); + await page.evaluate(`window.term.selectAll()`); + // Selection only cell needs to be first to ensure renderer has kicked in + await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); + await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]); + }); + }); + + describe('allowTransparency', async () => { + before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true})); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + it('transparent background inverse', async () => { + const theme: ITheme = { + background: '#ff000080' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + const data = `\\x1b[7m█\x1b[0m`; + await writeSync(data); + // Inverse background should be opaque + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); }); }); }); @@ -161,3 +893,48 @@ async function getCellColor(col: number, row: number): Promise { `); return await page.evaluate(`Array.from(window.result)`); } + +async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise { + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + args: [`--window-size=${width},${height}`, `--no-sandbox`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + await page.goto(APP); + await openTerminal(options); + await page.evaluate(` + window.addon = new WebglAddon(true); + window.term.loadAddon(window.addon); + `); +} + +export async function pollFor(page: puppeteer.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise): Promise { + if (preFn) { + await preFn(); + } + const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn(); + if (!deepEqual(result, val)) { + return new Promise(r => { + setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1); + }); + } +} + +const COLORS_16_TO_255 = [ + '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', + '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f', + '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff', + '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af', + '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', + '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff', + '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af', + '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f', + '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff', + '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af', + '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f', + '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff', + '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af', + '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e', + '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee' +]; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 3d41e1b55..ae85d9a01 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -11,19 +11,15 @@ import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel'; +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 { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CODE } from 'common/buffer/Constants'; -import { Terminal } from 'xterm'; -import { getLuminance } from './ColorUtils'; +import { NULL_CELL_CODE } from 'common/buffer/Constants'; +import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; -import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; +import { IRenderDimensions, IRenderer, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; import { IColorSet } from 'browser/Types'; -import { FLAGS } from './Constants'; -import { getCompatAttr } from './CharDataCompat'; - -export const INDICIES_PER_CELL = 4; +import { EventEmitter } from 'common/EventEmitter'; +import { CellData } from 'common/buffer/CellData'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -31,6 +27,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _devicePixelRatio: number; private _model: RenderModel = new RenderModel(); + private _workCell: CellData = new CellData(); private _canvas: HTMLCanvasElement; private _gl: IWebGL2RenderingContext; @@ -40,6 +37,10 @@ export class WebglRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; private _core: ITerminal; + private _isAttached: boolean; + + private _onRequestRefreshRows = new EventEmitter(); + public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } constructor( private _terminal: Terminal, @@ -50,11 +51,9 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core = (this._terminal)._core; - this._applyBgLuminanceBasedSelection(); - this._renderLayers = [ new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core), - new CursorRenderLayer(this._core.screenElement, 3, this._colors) + new CursorRenderLayer(this._core.screenElement, 3, this._colors, this._onRequestRefreshRows) ]; this.dimensions = { scaledCharWidth: 0, @@ -91,6 +90,8 @@ export class WebglRenderer extends Disposable implements IRenderer { // Update dimensions and acquire char atlas this.onCharSizeChanged(); + + this._isAttached = document.body.contains(this._core.screenElement); } public dispose(): void { @@ -99,20 +100,12 @@ export class WebglRenderer extends Disposable implements IRenderer { super.dispose(); } - private _applyBgLuminanceBasedSelection(): void { - // HACK: This is needed until webgl renderer adds support for selection colors - if (getLuminance(this._colors.background) > 0.5) { - this._colors.selection = { css: '#000', rgba: 255 }; - } else { - this._colors.selection = { css: '#fff', rgba: 4294967295 }; - } + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._charAtlas?.cacheCanvas; } public setColors(colors: IColorSet): void { this._colors = colors; - - this._applyBgLuminanceBasedSelection(); - // Clear layers and force a full render this._renderLayers.forEach(l => { l.setColors(this._terminal, this._colors); @@ -185,8 +178,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode); this._glyphRenderer.updateSelection(this._model, columnSelectMode); - // TODO: #2102 Should this move to RenderCoordinator? - this._core.refresh(0, this._terminal.rows - 1); + this._onRequestRefreshRows.fire({ start: 0, end: this._terminal.rows - 1 }); } public onCursorMove(): void { @@ -206,6 +198,8 @@ export class WebglRenderer extends Disposable implements IRenderer { */ private _refreshCharAtlas(): void { if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) { + // Mark as not attached so char atlas gets refreshed on next render + this._isAttached = false; return; } @@ -231,6 +225,16 @@ export class WebglRenderer extends Disposable implements IRenderer { } public renderRows(start: number, end: number): void { + if (!this._isAttached) { + if (document.body.contains(this._core.screenElement) && (this._core)._charSizeService.width && (this._core)._charSizeService.height) { + this._updateDimensions(); + this._refreshCharAtlas(); + this._isAttached = true; + } else { + return; + } + } + // Update render layers this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); @@ -255,52 +259,34 @@ export class WebglRenderer extends Disposable implements IRenderer { const line = terminal.buffer.lines.get(row)!; this._model.lineLengths[y] = 0; for (let x = 0; x < terminal.cols; x++) { - const charData = line.get(x); - const chars = charData[CHAR_DATA_CHAR_INDEX]; - let code = charData[CHAR_DATA_CODE_INDEX]; - const attr = getCompatAttr(line, x); // charData[CHAR_DATA_ATTR_INDEX]; - const i = ((y * terminal.cols) + x) * INDICIES_PER_CELL; + line.loadCell(x, this._workCell); + + const chars = this._workCell.getChars(); + let code = this._workCell.getCode(); + const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; } // Nothing has changed, no updates needed - if (this._model.cells[i] === code && this._model.cells[i + 1] === attr) { + 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) { continue; } - // Resolve bg and fg and cache in the model - const flags = attr >> 18; - let bg = attr & 0x1ff; - let fg = (attr >> 9) & 0x1ff; - - // If inverse flag is on, the foreground should become the background. - if (flags & FLAGS.INVERSE) { - const temp = bg; - bg = fg; - fg = temp; - if (fg === DEFAULT_COLOR) { - fg = INVERTED_DEFAULT_COLOR; - } - if (bg === DEFAULT_COLOR) { - bg = INVERTED_DEFAULT_COLOR; - } - } - const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && !!(flags & FLAGS.BOLD) && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; - fg += drawInBrightColor ? 8 : 0; - // Flag combined chars with a bit mask so they're easily identifiable if (chars.length > 1) { code = code | COMBINED_CHAR_BIT_MASK; } - this._model.cells[i ] = code; - this._model.cells[i + 1] = attr; - this._model.cells[i + 2] = bg; - this._model.cells[i + 3] = fg; + // 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._glyphRenderer.updateCell(x, y, code, attr, bg, fg, chars); + this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars); } } this._rectangleRenderer.updateBackgrounds(this._model); diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 857fa6287..8e26b1d6a 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -4,7 +4,7 @@ */ import { ICharAtlasConfig } from './Types'; -import { DEFAULT_COLOR } from 'common/buffer/Constants'; +import { Attributes } from 'common/buffer/Constants'; import { Terminal, FontWeight } from 'xterm'; import { IColorSet, IColor } from 'browser/Types'; @@ -21,9 +21,11 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number cursor: NULL_COLOR, cursorAccent: NULL_COLOR, selection: NULL_COLOR, + selectionOpaque: NULL_COLOR, // 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() + ansi: colors.ansi.slice(), + contrastCache: colors.contrastCache }; return { devicePixelRatio: window.devicePixelRatio, @@ -34,6 +36,8 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number fontWeight: terminal.getOption('fontWeight') as FontWeight, fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight, allowTransparency: terminal.getOption('allowTransparency'), + drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'), + minimumContrastRatio: terminal.getOption('minimumContrastRatio'), colors: clonedColors }; } @@ -52,10 +56,12 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean a.allowTransparency === b.allowTransparency && a.scaledCharWidth === b.scaledCharWidth && a.scaledCharHeight === b.scaledCharHeight && + a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors && + a.minimumContrastRatio === b.minimumContrastRatio && a.colors.foreground === b.colors.foreground && a.colors.background === b.colors.background; } export function is256Color(colorCode: number): boolean { - return colorCode < DEFAULT_COLOR; + return (colorCode & Attributes.CM_MASK) === Attributes.CM_P16 || (colorCode & Attributes.CM_MASK) === Attributes.CM_P256; } diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts index 1de843e08..cd73393ce 100644 --- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts +++ b/addons/xterm-addon-webgl/src/atlas/Types.d.ts @@ -25,5 +25,7 @@ export interface ICharAtlasConfig { scaledCharWidth: number; scaledCharHeight: number; allowTransparency: boolean; + drawBoldTextInBrightColors: boolean; + minimumContrastRatio: number; colors: IColorSet; } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 8d8e995bf..6c3369f0d 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -4,14 +4,14 @@ */ import { ICharAtlasConfig } from './Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; +import { DIM_OPACITY } from 'browser/renderer/atlas/Constants'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; -import { DEFAULT_COLOR, DEFAULT_ATTR } from 'common/buffer/Constants'; -import { is256Color } from './CharAtlasUtils'; +import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'browser/Types'; -import { FLAGS } from '../Constants'; import { IDisposable } from 'xterm'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { channels, rgba } from 'browser/Color'; // 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. @@ -68,8 +68,12 @@ export class WebglCharAtlas implements IDisposable { public hasCanvasChanged = false; private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; + private _workAttributeData: AttributeData = new AttributeData(); - constructor(document: Document, private _config: ICharAtlasConfig) { + constructor( + document: Document, + private _config: ICharAtlasConfig + ) { this.cacheCanvas = document.createElement('canvas'); this.cacheCanvas.width = TEXTURE_WIDTH; this.cacheCanvas.height = TEXTURE_HEIGHT; @@ -82,9 +86,6 @@ export class WebglCharAtlas implements IDisposable { 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 is useful for debugging - document.body.appendChild(this.cacheCanvas); } public dispose(): void { @@ -103,9 +104,11 @@ export class WebglCharAtlas implements IDisposable { protected _doWarmUp(): void { // Pre-fill with ASCII 33-126 for (let i = 33; i < 126; i++) { - const rasterizedGlyph = this._drawToCache(i, DEFAULT_ATTR, DEFAULT_COLOR, DEFAULT_COLOR); + const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR); this._cacheMap[i] = { - [DEFAULT_ATTR]: rasterizedGlyph + [DEFAULT_COLOR]: { + [DEFAULT_COLOR]: rasterizedGlyph + } }; } } @@ -123,16 +126,23 @@ export class WebglCharAtlas implements IDisposable { return false; } - public getRasterizedGlyphCombinedChar(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph { + public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph { let rasterizedGlyphSet = this._cacheMapCombined[chars]; if (!rasterizedGlyphSet) { rasterizedGlyphSet = {}; this._cacheMapCombined[chars] = rasterizedGlyphSet; } - let rasterizedGlyph = rasterizedGlyphSet[attr]; + let rasterizedGlyph: IRasterizedGlyph | undefined; + const rasterizedGlyphSetBg = rasterizedGlyphSet[bg]; + if (rasterizedGlyphSetBg) { + rasterizedGlyph = rasterizedGlyphSetBg[fg]; + } if (!rasterizedGlyph) { - rasterizedGlyph = this._drawToCache(chars, attr, bg, fg); - rasterizedGlyphSet[attr] = rasterizedGlyph; + rasterizedGlyph = this._drawToCache(chars, bg, fg); + if (!rasterizedGlyphSet[bg]) { + rasterizedGlyphSet[bg] = {}; + } + rasterizedGlyphSet[bg]![fg] = rasterizedGlyph; } return rasterizedGlyph; } @@ -140,16 +150,23 @@ export class WebglCharAtlas implements IDisposable { /** * Gets the glyphs texture coords, drawing the texture if it's not already */ - public getRasterizedGlyph(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph { + public getRasterizedGlyph(code: number, bg: number, fg: number): IRasterizedGlyph { let rasterizedGlyphSet = this._cacheMap[code]; if (!rasterizedGlyphSet) { rasterizedGlyphSet = {}; this._cacheMap[code] = rasterizedGlyphSet; } - let rasterizedGlyph = rasterizedGlyphSet[attr]; + let rasterizedGlyph: IRasterizedGlyph | undefined; + const rasterizedGlyphSetBg = rasterizedGlyphSet[bg]; + if (rasterizedGlyphSetBg) { + rasterizedGlyph = rasterizedGlyphSetBg[fg]; + } if (!rasterizedGlyph) { - rasterizedGlyph = this._drawToCache(code, attr, bg, fg); - rasterizedGlyphSet[attr] = rasterizedGlyph; + rasterizedGlyph = this._drawToCache(code, bg, fg); + if (!rasterizedGlyphSet[bg]) { + rasterizedGlyphSet[bg] = {}; + } + rasterizedGlyphSet[bg]![fg] = rasterizedGlyph; } return rasterizedGlyph; } @@ -161,48 +178,165 @@ export class WebglCharAtlas implements IDisposable { return this._config.colors.ansi[idx]; } - private _getBackgroundColor(bg: number): IColor { + private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean): IColor { if (this._config.allowTransparency) { // The background color might have some transparency, so we need to render it as fully // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // around the anti-aliased edges of the glyph, and it would look too dark. return TRANSPARENT_COLOR; - } else if (bg === INVERTED_DEFAULT_COLOR) { - return this._config.colors.foreground; - } else if (is256Color(bg)) { - return this._getColorFromAnsiIndex(bg); } - // TODO: Support true color - return this._config.colors.background; + + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + return this._getColorFromAnsiIndex(bgColor); + case Attributes.CM_RGB: + const arr = AttributeData.toColorRGB(bgColor); + // TODO: This object creation is slow + return { + rgba: bgColor << 8, + css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}` + }; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._config.colors.foreground; + } + return this._config.colors.background; + } } - private _getForegroundColor(fg: number): IColor { - if (fg === INVERTED_DEFAULT_COLOR) { - return this._config.colors.background; - } else if (is256Color(fg)) { - return this._getColorFromAnsiIndex(fg); + private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { + const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); + if (minimumContrastCss) { + return minimumContrastCss; + } + + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } + return this._getColorFromAnsiIndex(fgColor).css; + case Attributes.CM_RGB: + const arr = AttributeData.toColorRGB(fgColor); + return channels.toCss(arr[0], arr[1], arr[2]); + case Attributes.CM_DEFAULT: + default: + if (inverse) { + const bg = this._config.colors.background.css; + if (bg.length === 9) { + // Remove bg alpha channel if present + return bg.substr(0, 7); + } + return bg; + } + return this._config.colors.foreground.css; } - // TODO: Support true color - return this._config.colors.foreground; } - private _drawToCache(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph; - private _drawToCache(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph; - private _drawToCache(codeOrChars: number | string, attr: number, bg: number, fg: number): IRasterizedGlyph { + private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number { + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + return this._getColorFromAnsiIndex(bgColor).rgba; + case Attributes.CM_RGB: + return bgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._config.colors.foreground.rgba; + } + return this._config.colors.background.rgba; + } + } + + private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } + return this._getColorFromAnsiIndex(fgColor).rgba; + case Attributes.CM_RGB: + return fgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._config.colors.background.rgba; + } + return this._config.colors.foreground.rgba; + } + } + + private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined { + if (this._config.minimumContrastRatio === 1) { + return undefined; + } + + // Try get from cache first + const adjustedColor = this._config.colors.contrastCache.getCss(bg, fg); + if (adjustedColor !== undefined) { + return adjustedColor || undefined; + } + + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse); + const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold); + const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio); + + if (!result) { + this._config.colors.contrastCache.setCss(bg, fg, null); + return undefined; + } + + const css = channels.toCss( + (result >> 24) & 0xFF, + (result >> 16) & 0xFF, + (result >> 8) & 0xFF + ); + this._config.colors.contrastCache.setCss(bg, fg, css); + + return css; + } + + private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph; + private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph; + private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; this.hasCanvasChanged = true; - const flags = attr >> 18; - - const bold = !!(flags & FLAGS.BOLD); - const dim = !!(flags & FLAGS.DIM); - const italic = !!(flags & FLAGS.ITALIC); - this._tmpCtx.save(); + this._workAttributeData.fg = fg; + this._workAttributeData.bg = bg; + + const invisible = !!this._workAttributeData.isInvisible(); + if (invisible) { + return NULL_RASTERIZED_GLYPH; + } + + const bold = !!this._workAttributeData.isBold(); + const inverse = !!this._workAttributeData.isInverse(); + const dim = !!this._workAttributeData.isDim(); + const italic = !!this._workAttributeData.isItalic(); + let fgColor = this._workAttributeData.getFgColor(); + let fgColorMode = this._workAttributeData.getFgColorMode(); + let bgColor = this._workAttributeData.getBgColor(); + let bgColorMode = this._workAttributeData.getBgColorMode(); + if (inverse) { + const temp = fgColor; + fgColor = bgColor; + bgColor = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; + } + // draw the background - const backgroundColor = this._getBackgroundColor(bg); + const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse); // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of // transparency in backgroundColor this._tmpCtx.globalCompositeOperation = 'copy'; @@ -217,7 +351,7 @@ export class WebglCharAtlas implements IDisposable { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = 'top'; - this._tmpCtx.fillStyle = this._getForegroundColor(fg).css; + this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); // Apply alpha to dim the character if (dim) { @@ -398,3 +532,8 @@ function clearColor(imageData: ImageData, color: IColor): boolean { } return isEmpty; } + +function toPaddedHex(c: number): string { + const s = c.toString(16); + return s.length < 2 ? '0' + s : s; +} diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 0e2d645e5..b897ed44e 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -4,7 +4,6 @@ */ import { IRenderLayer } from './Types'; -import { IGlyphIdentifier } from '../atlas/Types'; import { acquireCharAtlas } from '../atlas/CharAtlasCache'; import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; @@ -25,19 +24,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _charAtlas: WebglCharAtlas | undefined; - /** - * An object that's reused when drawing glyphs in order to reduce GC. - */ - private _currentGlyphIdentifier: IGlyphIdentifier = { - chars: '', - code: 0, - bg: 0, - fg: 0, - bold: false, - dim: false, - italic: false - }; - constructor( private _container: HTMLElement, id: string, @@ -167,11 +153,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to fill. * @param y The row to fill. */ - protected _fillLeftLineAtCell(x: number, y: number): void { + protected _fillLeftLineAtCell(x: number, y: number, width: number): void { this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, - window.devicePixelRatio, + window.devicePixelRatio * width, this._scaledCellHeight); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 6aed5f53c..1072a1719 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -8,7 +8,8 @@ import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; +import { IEventEmitter } from 'common/EventEmitter'; interface ICursorState { x: number; @@ -29,7 +30,12 @@ export class CursorRenderLayer extends BaseRenderLayer { private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; private _cell: ICellData = new CellData(); - constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { + constructor( + container: HTMLElement, + zIndex: number, + colors: IColorSet, + private _onRequestRefreshRowsEvent: IEventEmitter + ) { super(container, 'cursor', zIndex, true, colors); this._state = { x: 0, @@ -70,14 +76,14 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.pause(); } - terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); + this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY }); } public onFocus(terminal: Terminal): void { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.resume(terminal); } else { - terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); + this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY }); } } @@ -89,13 +95,12 @@ export class CursorRenderLayer extends BaseRenderLayer { }); } } else { - if (this._cursorBlinkStateManager) { - this._cursorBlinkStateManager.dispose(); - } - // Request a refresh from the terminal as management of rendering is being - // moved back to the terminal - terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); + this._cursorBlinkStateManager?.dispose(); + this._cursorBlinkStateManager = undefined; } + // Request a refresh from the terminal as management of rendering is being + // moved back to the terminal + this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY }); } public onCursorMove(terminal: Terminal): void { @@ -115,7 +120,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.cursorState || (terminal as any)._core.cursorHidden) { + if (!(terminal as any)._core._coreService.isCursorInitialized || (terminal as any)._core._coreService.isCursorHidden) { this._clearCursor(); return; } @@ -139,12 +144,17 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell); + const cursorStyle = terminal.getOption('cursorStyle'); + if (cursorStyle && cursorStyle !== 'block') { + this._cursorRenderers[cursorStyle](terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell); + } else { + this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell); + } this._ctx.restore(); this._state.x = terminal.buffer.cursorX; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; - this._state.style = terminal.getOption('cursorStyle'); + this._state.style = cursorStyle; this._state.width = this._cell.getWidth(); return; } @@ -194,7 +204,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._fillLeftLineAtCell(x, y); + this._fillLeftLineAtCell(x, y, terminal.getOption('cursorWidth')); this._ctx.restore(); } 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 5199a2609..9586433a9 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -10,6 +10,8 @@ declare module 'xterm-addon-webgl' { * An xterm.js addon that provides search functionality. */ export class WebglAddon implements ITerminalAddon { + public textureAtlas?: HTMLCanvasElement; + constructor(preserveDrawingBuffer?: boolean); /** diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 26f0ca73a..f6f892c7e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,59 +7,78 @@ jobs: - job: Linux pool: vmImage: 'ubuntu-16.04' + variables: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 steps: - task: NodeTool@0 inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + - task: YarnInstaller@3 inputs: - versionSpec: "1.9.4" + versionSpec: '1.x' displayName: 'Install Yarn' - - script: | - yarn + - task: CacheBeta@1 + inputs: + key: yarn2 | $(Agent.OS) | yarn.lock + path: node_modules + displayName: Cache node modules + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: | - yarn test-unit + yarn test-unit-coverage --forbid-only + ./node_modules/.bin/nyc report --reporter=cobertura displayName: 'Unit tests' - - script: | - yarn lint + - script: yarn lint displayName: 'Lint' + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml' + displayName: 'Publish coverage' - job: macOS pool: vmImage: 'xcode9-macos10.13' + variables: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 steps: - task: NodeTool@0 inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - script: | - yarn + - task: CacheBeta@1 + inputs: + key: yarn2 | $(Agent.OS) | yarn.lock + path: node_modules + displayName: Cache node modules + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - - script: | - yarn test-unit + - script: yarn test-unit --forbid-only displayName: 'Unit tests' - - script: | - yarn lint + - script: yarn lint displayName: 'Lint' - job: Windows pool: vmImage: 'vs2017-win2016' + variables: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 steps: - task: NodeTool@0 inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - script: | - yarn + - task: CacheBeta@1 + inputs: + key: yarn2 | $(Agent.OS) | yarn.lock + path: node_modules + displayName: Cache node modules + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - - script: | - yarn test-unit + - script: yarn test-unit --forbid-only displayName: 'Unit tests' - - script: | - yarn lint + - script: yarn lint displayName: 'Lint' - job: Linux_IntegrationTests @@ -70,17 +89,16 @@ jobs: inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + - task: YarnInstaller@3 inputs: - versionSpec: "1.9.4" + versionSpec: '1.x' displayName: 'Install Yarn' - - script: | - yarn + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: | yarn start & sleep 10 - yarn test-api --headless + yarn test-api --headless --forbid-only displayName: 'Linux Integration tests' - job: macOS_IntegrationTests @@ -91,13 +109,12 @@ jobs: inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - script: | - yarn + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: | yarn start & sleep 10 - yarn test-api --headless + yarn test-api --headless --forbid-only displayName: 'MacOS Integration tests' - job: Release @@ -107,21 +124,26 @@ jobs: - Windows - Linux_IntegrationTests - macOS_IntegrationTests - condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/*'))) + condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true'))) pool: vmImage: 'ubuntu-16.04' + variables: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 steps: - task: NodeTool@0 inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + - task: YarnInstaller@3 inputs: - versionSpec: "1.9.4" + versionSpec: '1.x' displayName: 'Install Yarn' - - script: | - yarn + - task: CacheBeta@1 + inputs: + key: yarn2 | $(Agent.OS) | yarn.lock + path: node_modules + displayName: Cache node modules + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - - script: | - NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js + - script: NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js displayName: 'Package and publish to npm' diff --git a/bin/install-addons.js b/bin/install-addons.js new file mode 100644 index 000000000..83adea1f0 --- /dev/null +++ b/bin/install-addons.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + * + * Script to initialize addon packages under "addons/" with outer deps. + */ + +const path = require('path'); +const cp = require('child_process'); +const fs = require('fs'); + +const PACKAGE_ROOT = path.join(__dirname, '..'); + +// install addon deps +const addonsPath = path.join(PACKAGE_ROOT, 'addons'); +if (fs.existsSync(addonsPath)) { + console.log('pulling addon dependencies...'); + + // whether to use yarn or npm + let hasYarn = false; + try { + cp.execSync('yarn --version').toString(); + hasYarn = true; + } catch(e) {} + + // walk all addon folders + fs.readdir(addonsPath, (err, files) => { + files.forEach(folder => { + const addonPath = path.join(addonsPath, folder); + + // install only if there are dependencies listed + let packageJson; + try { + packageJson = require(path.join(addonPath, 'package.json')); + } catch (e) { + // swallow as changing branches can leave folders around + } + if (packageJson + && ( + (packageJson.devDependencies && Object.keys(packageJson.devDependencies).length) + || (packageJson.dependencies && Object.keys(packageJson.dependencies).length) + ) + ) + { + console.log('Preparing', folder); + if (hasYarn) { + cp.execSync('yarn', {cwd: addonPath}); + } else { + cp.execSync('npm install', {cwd: addonPath}); + } + } else { + console.log('Skipped', folder); + } + }); + }); +} diff --git a/bin/publish.js b/bin/publish.js index d95ecc574..9050c344e 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -5,6 +5,7 @@ const cp = require('child_process'); const fs = require('fs'); +const os = require('os'); const path = require('path'); // Setup auth @@ -18,8 +19,9 @@ if (isDryRun) { const changedFiles = getChangedFilesInCommit('HEAD'); // Publish xterm if any files were changed outside of the addons directory +let isStableRelease = false; if (changedFiles.some(e => e.search(/^addons\//) === -1)) { - checkAndPublishPackage(path.resolve(__dirname, '..')); + isStableRelease = checkAndPublishPackage(path.resolve(__dirname, '..')); } // Publish addons if any files were changed inside of the addon @@ -39,6 +41,11 @@ addonPackageDirs.forEach(p => { } }); +// Publish website if it's a stable release +if (isStableRelease) { + updateWebsite(); +} + function checkAndPublishPackage(packageDir) { const packageJson = require(path.join(packageDir, 'package.json')); @@ -76,6 +83,8 @@ function checkAndPublishPackage(packageDir) { } console.groupEnd(); + + return isStableRelease; } function getNextBetaVersion(packageJson) { @@ -88,7 +97,7 @@ function getNextBetaVersion(packageJson) { const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); if (publishedVersions.length === 0) { - return `${nextStableVersion}-${tag}1`; + return `${nextStableVersion}-${tag}.1`; } const latestPublishedVersion = publishedVersions.sort((a, b) => { const aVersion = parseInt(a.substr(a.search(/[0-9]+$/))); @@ -96,14 +105,14 @@ function getNextBetaVersion(packageJson) { return aVersion > bVersion ? -1 : 1; })[0]; const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10); - return `${nextStableVersion}-${tag}${latestTagVersion + 1}`; + return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`; } function getPublishedVersions(packageJson, version, tag) { const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']); const versionsJson = JSON.parse(versionsProcess.stdout); if (tag) { - return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`))); + return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}.[0-9]+`))); } return versionsJson; } @@ -115,3 +124,12 @@ function getChangedFilesInCommit(commit) { const changedFiles = output.split('\n').filter(e => e.length > 0); return changedFiles; } + +function updateWebsite() { + console.log('Updating website'); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'website-')); + const packageJson = require(path.join(path.resolve(__dirname, '..'), 'package.json')); + if (!isDryRun) { + cp.spawnSync('sh', [path.join(__dirname, 'update-website.sh'), packageJson.version], { cwd, stdio: [process.stdin, process.stdout, process.stderr] }); + } +} diff --git a/bin/test.js b/bin/test.js index 775ec004b..bd1784fbb 100644 --- a/bin/test.js +++ b/bin/test.js @@ -6,6 +6,8 @@ const cp = require('child_process'); const path = require('path'); +const COVERAGE_LINES_THRESHOLD = 60; + // Add `out` to the NODE_PATH so absolute paths can be resolved. const env = { ...process.env }; env.NODE_PATH = path.resolve(__dirname, '../out'); @@ -15,15 +17,42 @@ let testFiles = [ './out/**/*test.js' ]; -// ability to inject particular test files via -// yarn test [testFileA testFileB ...] +let flagArgs = []; + if (process.argv.length > 2) { - testFiles = process.argv.slice(2); + const args = process.argv.slice(2); + flagArgs = args.filter(e => e.startsWith('--')); + // ability to inject particular test files via + // yarn test [testFileA testFileB ...] + files = args.filter(e => !e.startsWith('--')); + if (files.length) { + testFiles = files; + } +} + +const checkCoverage = flagArgs.indexOf('--coverage') >= 0; + +if (checkCoverage) { + flagArgs.splice(flagArgs.indexOf('--coverage'), 1); + const executable = npmBinScript('nyc'); + const args = ['--check-coverage', `--lines=${COVERAGE_LINES_THRESHOLD}`, npmBinScript('mocha'), ...testFiles, ...flagArgs]; + console.info('executable', executable); + console.info('args', args); + const run = cp.spawnSync( + executable, + args, + { + cwd: path.resolve(__dirname, '..'), + env, + stdio: 'inherit' + } + ); + process.exit(run.status); } const run = cp.spawnSync( - path.resolve(__dirname, '../node_modules/.bin/mocha'), - testFiles, + npmBinScript('mocha'), + [...testFiles, ...flagArgs], { cwd: path.resolve(__dirname, '..'), env, @@ -31,4 +60,8 @@ const run = cp.spawnSync( } ); -process.exit(run.status); \ No newline at end of file +function npmBinScript(script) { + return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script)); +} + +process.exit(run.status); diff --git a/bin/update-website.sh b/bin/update-website.sh new file mode 100644 index 000000000..4379d9155 --- /dev/null +++ b/bin/update-website.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# Name the arguments +VERSION=$1 + +# Clone docs repo and update the documentation +git clone https://github.com/xtermjs/xtermjs.org +cd xtermjs.org +yarn +./bin/update-docs + +# Add changes to index and only proceed if there are changes to commit +touch test-file +git add . +if ! git diff-index --quiet HEAD --; then + + # Delete the upstream branch if it exists for some reason + export BRANCH_NAME=update-$VERSION + git branch -D $BRANCH_NAME || true + git push origin :$BRANCH_NAME || true + + # Create commit and push it to update-x.y.z + git checkout -b $BRANCH_NAME + git config --global user.name Daniel Imms + git config --global user.email tyriar@tyriar.com + git commit -m 'Update docs for v$VERSION' + git push --set-upstream origin update-4.2.0 + git push -f + + # Create a PR in the GitHub repo + curl \ + -H "Authorization: token $GITHUB_TOKEN" \ + -X POST \ + -d "{\"title\":\"Update docs for v$VERSION\",\"base\":\"master\",\"head\":\"xtermjs:$BRANCH_NAME\"}" \ + https://api.github.com/repos/xtermjs/xtermjs.org/pulls + +else + + echo "No changes to commit" + +fi diff --git a/demo/client.ts b/demo/client.ts index 92bac731b..6d146264b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -39,13 +39,39 @@ export interface IWindowWithTerminal extends Window { declare let window: IWindowWithTerminal; let term; -let fitAddon: FitAddon; -let searchAddon: SearchAddon; let protocol; let socketURL; let socket; let pid; +type AddonType = 'attach' | 'fit' | 'search' | 'web-links' | 'webgl'; + +interface IDemoAddon { + name: T; + canChange: boolean; + ctor: + T extends 'attach' ? typeof AttachAddon : + T extends 'fit' ? typeof FitAddon : + T extends 'search' ? typeof SearchAddon : + T extends 'web-links' ? typeof WebLinksAddon : + typeof WebglAddon; + instance?: + T extends 'attach' ? AttachAddon : + T extends 'fit' ? FitAddon : + T extends 'search' ? SearchAddon : + T extends 'web-links' ? WebLinksAddon : + T extends 'webgl' ? WebglAddon : + never; +} + +const addons: { [T in AddonType]: IDemoAddon} = { + attach: { name: 'attach', ctor: AttachAddon, canChange: false }, + fit: { name: 'fit', ctor: FitAddon, canChange: false }, + search: { name: 'search', ctor: SearchAddon, canChange: true }, + 'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true }, + webgl: { name: 'webgl', ctor: WebglAddon, canChange: true } +}; + const terminalContainer = document.getElementById('terminal-container'); const actionElements = { findNext: document.querySelector('#find-next'), @@ -92,7 +118,6 @@ if (document.location.pathname === '/test') { } else { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); - document.getElementById('webgl').addEventListener('click', () => term.loadAddon(new WebglAddon())); } function createTerminal(): void { @@ -108,11 +133,12 @@ function createTerminal(): void { // Load addons const typedTerm = term as TerminalType; - typedTerm.loadAddon(new WebLinksAddon()); - searchAddon = new SearchAddon(); - typedTerm.loadAddon(searchAddon); - fitAddon = new FitAddon(); - typedTerm.loadAddon(fitAddon); + addons['web-links'].instance = new WebLinksAddon(); + addons.search.instance = new SearchAddon(); + addons.fit.instance = new FitAddon(); + typedTerm.loadAddon(addons['web-links'].instance); + typedTerm.loadAddon(addons.search.instance); + typedTerm.loadAddon(addons.fit.instance); window.term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { @@ -129,17 +155,17 @@ function createTerminal(): void { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - fitAddon.fit(); + addons.fit.instance!.fit(); term.focus(); addDomListener(paddingElement, 'change', setPadding); addDomListener(actionElements.findNext, 'keyup', (e) => { - searchAddon.findNext(actionElements.findNext.value, getSearchOptions(e)); + addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e)); }); addDomListener(actionElements.findPrevious, 'keyup', (e) => { - searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions(e)); + addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e)); }); // fit is called within a setTimeout, cols and rows need this. @@ -167,8 +193,10 @@ function createTerminal(): void { } function runRealTerminal(): void { - term.loadAddon(new AttachAddon(socket)); + addons.attach.instance = new AttachAddon(socket); + term.loadAddon(addons.attach.instance); term._initialized = true; + initAddons(term); } function runFakeTerminal(): void { @@ -177,6 +205,7 @@ function runFakeTerminal(): void { } term._initialized = true; + initAddons(term); term.prompt = () => { term.write('\r\n$ '); @@ -221,6 +250,7 @@ function initOptions(term: TerminalType): void { bellSound: null, bellStyle: ['none', 'sound'], cursorStyle: ['block', 'underline', 'bar'], + fastScrollModifier: ['alt', 'ctrl', 'shift', undefined], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], @@ -253,7 +283,7 @@ function initOptions(term: TerminalType): void { }); html += '
'; numberOptions.forEach(o => { - html += `
`; + html += `
`; }); html += '
'; Object.keys(stringOptions).forEach(o => { @@ -282,7 +312,7 @@ function initOptions(term: TerminalType): void { console.log('change', o, input.value); if (o === 'cols' || o === 'rows') { updateTerminalSize(); - } else if (o === 'lineHeight') { + } else if (o === 'lineHeight' || o === 'scrollSensitivity') { term.setOption(o, parseFloat(input.value)); updateTerminalSize(); } else { @@ -299,6 +329,48 @@ function initOptions(term: TerminalType): void { }); } +function initAddons(term: TerminalType): void { + const fragment = document.createDocumentFragment(); + Object.keys(addons).forEach((name: AddonType) => { + const addon = addons[name]; + const checkbox = document.createElement('input') as HTMLInputElement; + checkbox.type = 'checkbox'; + checkbox.checked = !!addon.instance; + if (!addon.canChange) { + checkbox.disabled = true; + } + checkbox.addEventListener('change', () => { + if (checkbox.checked) { + addon.instance = new addon.ctor(); + term.loadAddon(addon.instance); + if (name === 'webgl') { + setTimeout(() => { + document.body.appendChild((addon.instance as WebglAddon).textureAtlas); + }, 0); + } + } else { + if (name === 'webgl') { + document.body.removeChild((addon.instance as WebglAddon).textureAtlas); + } + addon.instance!.dispose(); + addon.instance = undefined; + } + }); + const label = document.createElement('label'); + label.classList.add('addon'); + if (!addon.canChange) { + label.title = 'This addon is needed for the demo to operate'; + } + label.appendChild(checkbox); + label.appendChild(document.createTextNode(name)); + const wrapper = document.createElement('div'); + wrapper.classList.add('addon'); + wrapper.appendChild(label); + fragment.appendChild(wrapper); + }); + document.getElementById('addons-container').appendChild(fragment); +} + function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void { element.addEventListener(type, handler); term._core.register({ dispose: () => element.removeEventListener(type, handler) }); @@ -311,5 +383,5 @@ function updateTerminalSize(): void { const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; - fitAddon.fit(); + addons.fit.instance.fit(); } diff --git a/demo/index.html b/demo/index.html index ef8f3891c..88c6bc4c0 100644 --- a/demo/index.html +++ b/demo/index.html @@ -26,6 +26,11 @@

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

+
+

Addons

+

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

+
+

Style

@@ -36,7 +41,6 @@

Attention: The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.

- diff --git a/demo/server.js b/demo/server.js index 9a45d06fc..85e1aca7a 100644 --- a/demo/server.js +++ b/demo/server.js @@ -14,24 +14,26 @@ function startServer() { logs = {}; app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css')); - app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png')); + app.get('/logo.png', (req, res) => { // lgtm [js/missing-rate-limiting] + res.sendFile(__dirname + '/logo.png'); + }); - app.get('/', function(req, res){ + app.get('/', (req, res) => { // lgtm [js/missing-rate-limiting] res.sendFile(__dirname + '/index.html'); }); - app.get('/test', function(req, res){ + app.get('/test', (req, res) => { // lgtm [js/missing-rate-limiting] res.sendFile(__dirname + '/test.html'); }); - app.get('/style.css', function(req, res){ + app.get('/style.css', (req, res) => { // lgtm [js/missing-rate-limiting] res.sendFile(__dirname + '/style.css'); }); app.use('/dist', express.static(__dirname + '/dist')); app.use('/src', express.static(__dirname + '/src')); - app.post('/terminals', function (req, res) { + app.post('/terminals', (req, res) => { const env = Object.assign({}, process.env); env['COLORTERM'] = 'truecolor'; var cols = parseInt(req.query.cols), @@ -55,7 +57,7 @@ function startServer() { res.end(); }); - app.post('/terminals/:pid/size', function (req, res) { + app.post('/terminals/:pid/size', (req, res) => { var pid = parseInt(req.params.pid), cols = parseInt(req.query.cols), rows = parseInt(req.query.rows), diff --git a/fixtures/escape_sequence_files/t600-DECSTBM_SR.in b/fixtures/escape_sequence_files/t600-DECSTBM_SR.in new file mode 100644 index 000000000..9fa002fcc --- /dev/null +++ b/fixtures/escape_sequence_files/t600-DECSTBM_SR.in @@ -0,0 +1,25 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[5 A[5 A[5 A \ No newline at end of file diff --git a/fixtures/escape_sequence_files/t600-DECSTBM_SR.text b/fixtures/escape_sequence_files/t600-DECSTBM_SR.text new file mode 100644 index 000000000..8592dd1b4 --- /dev/null +++ b/fixtures/escape_sequence_files/t600-DECSTBM_SR.text @@ -0,0 +1,25 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB diff --git a/fixtures/escape_sequence_files/t601-DECSTBM_SL.in b/fixtures/escape_sequence_files/t601-DECSTBM_SL.in new file mode 100644 index 000000000..e99f51904 --- /dev/null +++ b/fixtures/escape_sequence_files/t601-DECSTBM_SL.in @@ -0,0 +1,25 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[5 @[5 @[5 @ \ No newline at end of file diff --git a/fixtures/escape_sequence_files/t601-DECSTBM_SL.text b/fixtures/escape_sequence_files/t601-DECSTBM_SL.text new file mode 100644 index 000000000..1912cb56c --- /dev/null +++ b/fixtures/escape_sequence_files/t601-DECSTBM_SL.text @@ -0,0 +1,25 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB diff --git a/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.in b/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.in new file mode 100644 index 000000000..e31f4e12c --- /dev/null +++ b/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.in @@ -0,0 +1,25 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[5'}[5'}[5'} \ No newline at end of file diff --git a/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.text b/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.text new file mode 100644 index 000000000..f3fa17bc8 --- /dev/null +++ b/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.text @@ -0,0 +1,25 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw +abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw +abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw +abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw +abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw +abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB diff --git a/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.in b/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.in new file mode 100644 index 000000000..ecb966d9e --- /dev/null +++ b/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.in @@ -0,0 +1,25 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[5'~[5'~[5'~ \ No newline at end of file diff --git a/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.text b/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.text new file mode 100644 index 000000000..b98db23c3 --- /dev/null +++ b/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.text @@ -0,0 +1,25 @@ +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB diff --git a/package.json b/package.json index e6ce25c5a..9f3c4a786 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "4.0.0", + "version": "4.3.0", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", @@ -11,13 +11,16 @@ "prepackage": "npm run build", "package": "webpack", "start": "node demo/start", - "lint": "tslint 'src/**/*.ts' 'addons/**/*.ts'", + "lint": "tslint 'src/**/*.ts' 'addons/*/src/**/*.ts'", "test": "npm run test-unit", "posttest": "npm run lint", "test-api": "mocha \"**/*.api.js\"", "test-unit": "node ./bin/test.js", + "test-unit-coverage": "node ./bin/test.js --coverage", "build": "tsc -b ./tsconfig.all.json", - "prepare": "npm run build", + "prepare": "npm run setup", + "setup": "npm run build", + "presetup": "node ./bin/install-addons.js", "prepublishOnly": "npm run package", "watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput", "benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json", @@ -28,6 +31,7 @@ }, "devDependencies": { "@types/chai": "^3.4.34", + "@types/deep-equal": "^1.0.1", "@types/glob": "^5.0.35", "@types/jsdom": "11.0.1", "@types/mocha": "^2.2.33", @@ -37,19 +41,21 @@ "@types/webpack": "^4.4.11", "@types/ws": "^6.0.1", "chai": "3.5.0", + "deep-equal": "^1.1.0", "express": "^4.17.1", "express-ws": "^4.0.0", "glob": "^7.0.5", "jsdom": "^11.11.0", "mocha": "^6.1.4", "mustache": "^3.0.1", - "node-pty": "0.7.6", + "node-pty": "^0.9.0", + "nyc": "13", "puppeteer": "^1.15.0", "source-map-loader": "^0.2.4", "ts-loader": "^6.0.4", "tslint": "^5.18.0", "tslint-consistent-codestyle": "^1.13.0", - "typescript": "3.5", + "typescript": "3.7", "utf8": "^3.0.0", "webpack": "^4.35.3", "webpack-cli": "^3.1.0", diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index fbbafc110..b7eeb352c 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -7,13 +7,13 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test'; import { Terminal } from './Terminal'; -import { IBufferLine } from 'common/Types'; +import { IBufferLine, IAttributeData } from 'common/Types'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService } from 'common/TestUtils.test'; import { IBufferService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; @@ -25,34 +25,46 @@ function getCursor(term: TestTerminal): number[] { ]; } +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; +} + +class TestInputHandler extends InputHandler { + get curAttrData(): IAttributeData { return (this as any)._curAttrData; } +} + describe('InputHandler', () => { describe('save and restore cursor', () => { const terminal = new MockInputHandlingTerminal(); - terminal.curAttrData.fg = 3; const bufferService = new MockBufferService(80, 30); bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; - const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new TestInputHandler(terminal, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + inputHandler.curAttrData.fg = 3; // Save cursor position inputHandler.saveCursor(); assert.equal(bufferService.buffer.x, 1); assert.equal(bufferService.buffer.y, 2); - assert.equal(terminal.curAttrData.fg, 3); + assert.equal(inputHandler.curAttrData.fg, 3); // Change cursor position bufferService.buffer.x = 10; bufferService.buffer.y = 20; - terminal.curAttrData.fg = 30; + inputHandler.curAttrData.fg = 30; // Restore cursor position inputHandler.restoreCursor(); assert.equal(bufferService.buffer.x, 1); assert.equal(bufferService.buffer.y, 2); - assert.equal(terminal.curAttrData.fg, 3); + assert.equal(inputHandler.curAttrData.fg, 3); }); describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { const optionsService = new MockOptionsService(); - const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService()); + const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService()); inputHandler.setCursorStyle(Params.fromArray([0])); assert.equal(optionsService.options['cursorStyle'], 'block'); @@ -93,7 +105,7 @@ describe('InputHandler', () => { it('should toggle Terminal.bracketedPasteMode', () => { const terminal = new MockInputHandlingTerminal(); terminal.bracketedPasteMode = false; - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, true); @@ -112,7 +124,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -150,7 +162,7 @@ describe('InputHandler', () => { it('deleteChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -191,7 +203,7 @@ describe('InputHandler', () => { it('eraseInLine', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // fill 6 lines to test 3 different states inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -220,7 +232,7 @@ describe('InputHandler', () => { it('eraseInDisplay', function(): void { const term = new Terminal({cols: 80, rows: 7}); const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // fill display with a's for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -355,7 +367,7 @@ describe('InputHandler', () => { describe('print', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); - const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); @@ -370,7 +382,7 @@ describe('InputHandler', () => { beforeEach(() => { term = new Terminal(); bufferService = new MockBufferService(80, 30); - handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + handler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); @@ -1125,13 +1137,6 @@ describe('InputHandler', () => { beforeEach(() => { term = new TestTerminal({cols: 10, rows: 10}); }); - 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; - } it('scrollUp', () => { term.writeSync('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(term), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']); @@ -1191,4 +1196,137 @@ describe('InputHandler', () => { assert.deepEqual(getLines(term), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); }); }); + 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']); + }); + }); + it('should parse big chunks in smaller subchunks', () => { + // max single chunk size is hardcoded as 131072 + const calls: any[] = []; + const term = new TestTerminal({cols: 10, rows: 10}); + (term as any)._inputHandler._parser.parse = (data: Uint32Array, length: number) => { + calls.push([data.length, length]); + }; + term.writeSync('12345'); + term.writeSync('a'.repeat(10000)); + term.writeSync('a'.repeat(200000)); + term.writeSync('a'.repeat(300000)); + assert.deepEqual(calls, [ + [4096, 5], + [10000, 10000], + [131072, 131072], [131072, 200000 - 131072], + [131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072] + ]); + }); + describe('should correctly reset cells taken by wide chars', () => { + let term: TestTerminal; + beforeEach(() => { + term = new TestTerminal({cols: 10, rows: 5, scrollback: 1}); + term.writeSync('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥'); + }); + it('print', () => { + term.writeSync('\x1b[H#'); + assert.deepEqual(getLines(term), ['# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[1;6H######'); + assert.deepEqual(getLines(term), ['# ¥ #####', '# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '##¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;9H#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥#', '¥¥¥¥¥', '']); + term.writeSync('#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '¥¥¥¥¥', '']); + term.writeSync('#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥¥', '']); + term.writeSync('\x1b[4;10H#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥ #', '']); + }); + it('EL', () => { + term.writeSync('\x1b[1;6H\x1b[K#'); + assert.deepEqual(getLines(term), ['¥¥ #', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[2;5H\x1b[1K'); + assert.deepEqual(getLines(term), ['¥¥ #', ' ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;6H\x1b[1K'); + assert.deepEqual(getLines(term), ['¥¥ #', ' ¥¥', ' ¥¥', '¥¥¥¥¥', '']); + }); + it('ICH', () => { + term.writeSync('\x1b[1;6H\x1b[@'); + assert.deepEqual(getLines(term), ['¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[2;4H\x1b[2@'); + assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;4H\x1b[3@'); + assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[4;4H\x1b[4@'); + assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥ ¥', '']); + }); + it('DCH', () => { + term.writeSync('\x1b[1;6H\x1b[P'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[2;6H\x1b[2P'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;6H\x1b[3P'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); + }); + it('ECH', () => { + term.writeSync('\x1b[1;6H\x1b[X'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[2;6H\x1b[2X'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;6H\x1b[3X'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7c4f57620..c04f11137 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -19,7 +19,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; @@ -32,10 +32,12 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * Document common VT features here that are currently unsupported */ // @vt: unsupported DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image starting at cursor position." -// @vt: unsupported CSI SL "Scroll Left" "CSI Ps SP @" "Scroll viewport `Ps` times to the left." -// @vt: unsupported CSI SR "Scroll Right" "CSI Ps SP A" "Scroll viewport `Ps` times to the right." -// @vt: unsupported CSI DECIC "Insert Columns" "CSI Ps ' }" "Insert `Ps` columns at cursor position." -// @vt: unsupported CSI DECDC "Delete Columns" "CSI Ps ' ~" "Delete `Ps` columns at cursor position." + +/** + * Max length of the UTF32 input buffer. Real memory consumption is 4 times higher. + */ +const MAX_PARSEBUFFER_LENGTH = 131072; + /** * DCS subparser implementations @@ -137,6 +139,15 @@ export class InputHandler extends Disposable implements IInputHandler { private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); private _workCell: CellData = new CellData(); + private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); + private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone(); + + 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 _onRequestBell = new EventEmitter(); + public get onRequestBell(): IEvent { return this._onRequestBell.event; } private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter(); @@ -145,8 +156,9 @@ export class InputHandler extends Disposable implements IInputHandler { public get onScroll(): IEvent { return this._onScroll.event; } constructor( - protected _terminal: IInputHandlingTerminal, + private _terminal: IInputHandlingTerminal, private readonly _bufferService: IBufferService, + private readonly _charsetService: ICharsetService, private readonly _coreService: ICoreService, private readonly _dirtyRowService: IDirtyRowService, private readonly _logService: ILogService, @@ -189,8 +201,12 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI handler */ this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params)); + // @vt: supported CSI SL "Scroll Left" "CSI Ps SP @" "Scroll viewport `Ps` times to the left." + this._parser.setCsiHandler({intermediates: ' ', final: '@'}, params => this.scrollLeft(params)); // @vt: supported CSI CUU "Cursor Up" "CSI Ps A" "Move cursor `Ps` times up (default=1)." this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params)); + // @vt: supported CSI SR "Scroll Right" "CSI Ps SP A" "Scroll viewport `Ps` times to the right." + this._parser.setCsiHandler({intermediates: ' ', final: 'A'}, params => this.scrollRight(params)); // @vt: supported CSI CUD "Cursor Down" "CSI Ps B" "Move cursor `Ps` times down (default=1)." this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params)); // @vt: supported CSI CUF "Cursor Forward" "CSI Ps C" "Move cursor `Ps` times forward (default=1)." @@ -321,6 +337,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params)); // @vt: partly CSI SCORC "Restore Cursor" "CSI u" "Restore cursor position, charmap and text attributes." this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params)); + // @vt: supported CSI DECIC "Insert Columns" "CSI Ps ' }" "Insert `Ps` columns at cursor position." + this._parser.setCsiHandler({intermediates: '\'', final: '}'}, params => this.insertColumns(params)); + // @vt: supported CSI DECDC "Delete Columns" "CSI Ps ' ~" "Delete `Ps` columns at cursor position." + this._parser.setCsiHandler({intermediates: '\'', final: '~'}, params => this.deleteColumns(params)); /** * execute handler @@ -426,7 +446,7 @@ export class InputHandler extends Disposable implements IInputHandler { 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.reset()); + 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)); @@ -471,35 +491,60 @@ export class InputHandler extends Disposable implements IInputHandler { this._logService.debug('parsing data', data); + // resize input buffer if needed if (this._parseBuffer.length < data.length) { - this._parseBuffer = new Uint32Array(data.length); + if (this._parseBuffer.length < MAX_PARSEBUFFER_LENGTH) { + this._parseBuffer = new Uint32Array(Math.min(data.length, MAX_PARSEBUFFER_LENGTH)); + } } - this._parser.parse(this._parseBuffer, - (typeof data === 'string') + + // Clear the dirty row service so we know which lines changed as a result of parsing + 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) { + 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); + } + } else { + const len = (typeof data === 'string') ? this._stringDecoder.decode(data, this._parseBuffer) - : this._utf8Decoder.decode(data, this._parseBuffer) - ); + : this._utf8Decoder.decode(data, this._parseBuffer); + this._parser.parse(this._parseBuffer, len); + } buffer = this._bufferService.buffer; if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { this._onCursorMove.fire(); } - this._terminal.refresh(this._dirtyRowService.start, this._dirtyRowService.end); + + // Refresh any dirty rows accumulated as part of parsing + this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end); } public print(data: Uint32Array, start: number, end: number): void { let code: number; let chWidth: number; const buffer = this._bufferService.buffer; - const charset = this._terminal.charset; + const charset = this._charsetService.charset; const screenReaderMode = this._optionsService.options.screenReaderMode; const cols = this._bufferService.cols; - const wraparoundMode = this._terminal.wraparoundMode; + const wraparoundMode = this._coreService.decPrivateModes.wraparound; const insertMode = this._terminal.insertMode; - const curAttr = this._terminal.curAttrData; + const curAttr = this._curAttrData; let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); this._dirtyRowService.markDirty(buffer.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); + } + for (let pos = start; pos < end; ++pos) { code = data[pos]; @@ -551,7 +596,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(true); + this._terminal.scroll(this._eraseAttrData(), true); } else { if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; @@ -575,7 +620,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)); + bufferRow.insertCells(buffer.x, chWidth, buffer.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 @@ -601,7 +646,7 @@ export class InputHandler extends Disposable implements IInputHandler { // This needs to check whether: // - fullwidth + surrogates: reset // - combining: only base char gets carried on (bug in xterm?) - if (end) { + if (end - start > 0) { bufferRow.loadCell(buffer.x - 1, this._workCell); if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) { this._parser.precedingCodepoint = 0; @@ -611,6 +656,12 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.precedingCodepoint = this._workCell.content; } } + + // 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); + } + this._dirtyRowService.markDirty(buffer.y); } @@ -647,7 +698,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Bell (Ctrl-G). */ public bell(): void { - this._terminal.bell(); + this._onRequestBell.fire(); } /** @@ -658,13 +709,14 @@ export class InputHandler extends Disposable implements IInputHandler { // make buffer local for faster access const buffer = this._bufferService.buffer; + this._dirtyRowService.markDirty(buffer.y); if (this._optionsService.options.convertEol) { buffer.x = 0; } buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(); + this._terminal.scroll(this._eraseAttrData()); } else if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } @@ -672,6 +724,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (buffer.x >= this._bufferService.cols) { buffer.x--; } + this._dirtyRowService.markDirty(buffer.y); this._onLineFeed.fire(); } @@ -716,7 +769,7 @@ export class InputHandler extends Disposable implements IInputHandler { * G1 character set. */ public shiftOut(): void { - this._terminal.setgLevel(1); + this._charsetService.setgLevel(1); } /** @@ -725,7 +778,7 @@ export class InputHandler extends Disposable implements IInputHandler { * character set (the default). */ public shiftIn(): void { - this._terminal.setgLevel(0); + this._charsetService.setgLevel(0); } /** @@ -733,16 +786,18 @@ export class InputHandler extends Disposable implements IInputHandler { */ private _restrictCursor(): void { this._bufferService.buffer.x = Math.min(this._bufferService.cols - 1, Math.max(0, this._bufferService.buffer.x)); - this._bufferService.buffer.y = this._terminal.originMode + 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); } /** * Set absolute cursor position. */ private _setCursor(x: number, y: number): void { - if (this._terminal.originMode) { + this._dirtyRowService.markDirty(this._bufferService.buffer.y); + if (this._coreService.decPrivateModes.origin) { this._bufferService.buffer.x = x; this._bufferService.buffer.y = this._bufferService.buffer.scrollTop + y; } else { @@ -750,6 +805,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y = y; } this._restrictCursor(); + this._dirtyRowService.markDirty(this._bufferService.buffer.y); } /** @@ -952,7 +1008,8 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( start, end, - this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() ); if (clearWrap) { line.isWrapped = false; @@ -966,7 +1023,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ private _resetBufferLine(y: number): void { const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + y); - line.fill(this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData())); + line.fill(this._bufferService.buffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } @@ -1081,7 +1138,7 @@ export class InputHandler extends Disposable implements IInputHandler { // 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._terminal.eraseAttrData())); + buffer.lines.splice(row, 0, buffer.getBlankLine(this._eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); @@ -1112,7 +1169,7 @@ export class InputHandler extends Disposable implements IInputHandler { // 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._terminal.eraseAttrData())); + buffer.lines.splice(j, 0, buffer.getBlankLine(this._eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); @@ -1130,7 +1187,8 @@ export class InputHandler extends Disposable implements IInputHandler { line.insertCells( this._bufferService.buffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } @@ -1147,7 +1205,8 @@ export class InputHandler extends Disposable implements IInputHandler { line.deleteCells( this._bufferService.buffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } @@ -1164,7 +1223,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } @@ -1173,18 +1232,108 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ public scrollDown(params: IParams): void { - if (params.length < 2) { - let param = params.params[0] || 1; + let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; + // 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(this._terminal.eraseAttrData())); - } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + while (param--) { + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + } + + /** + * CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48 + * + * Notation: (Pn) + * Representation: CSI Pn 02/00 04/00 + * Parameter default value: Pn = 1 + * SL causes the data in the presentation component to be moved by n character positions + * if the line orientation is horizontal, or by n line positions if the line orientation + * is vertical, such that the data appear to move to the left; where n equals the value of Pn. + * The active presentation position is not affected by this control function. + * + * Supported: + * - always left shift (no line orientation setting respected) + */ + public scrollLeft(params: IParams): void { + const buffer = this._bufferService.buffer; + if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + return; + } + 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()); + line.isWrapped = false; + } + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + } + + /** + * CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48 + * + * Notation: (Pn) + * Representation: CSI Pn 02/00 04/01 + * Parameter default value: Pn = 1 + * SR causes the data in the presentation component to be moved by n character positions + * if the line orientation is horizontal, or by n line positions if the line orientation + * is vertical, such that the data appear to move to the right; where n equals the value of Pn. + * The active presentation position is not affected by this control function. + * + * Supported: + * - always right shift (no line orientation setting respected) + */ + public scrollRight(params: IParams): void { + const buffer = this._bufferService.buffer; + if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + return; + } + 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()); + line.isWrapped = false; + } + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + } + + /** + * CSI Pm ' } + * Insert Ps Column(s) (default = 1) (DECIC), VT420 and up. + */ + public insertColumns(params: IParams): void { + const buffer = this._bufferService.buffer; + if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + return; + } + 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()); + line.isWrapped = false; + } + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + } + + /** + * CSI Pm ' ~ + * Delete Ps Column(s) (default = 1) (DECDC), VT420 and up. + */ + public deleteColumns(params: IParams): void { + const buffer = this._bufferService.buffer; + if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + return; + } + 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()); + line.isWrapped = false; + } + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } /** @@ -1198,7 +1347,8 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( this._bufferService.buffer.x, this._bufferService.buffer.x + (params.params[0] || 1), - this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } @@ -1411,34 +1561,32 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.applicationCursorKeys = true; break; case 2: - this._terminal.setgCharset(0, DEFAULT_CHARSET); - this._terminal.setgCharset(1, DEFAULT_CHARSET); - this._terminal.setgCharset(2, DEFAULT_CHARSET); - this._terminal.setgCharset(3, DEFAULT_CHARSET); + this._charsetService.setgCharset(0, DEFAULT_CHARSET); + this._charsetService.setgCharset(1, DEFAULT_CHARSET); + this._charsetService.setgCharset(2, DEFAULT_CHARSET); + this._charsetService.setgCharset(3, DEFAULT_CHARSET); // set VT100 mode here break; case 3: // 132 col mode // TODO: move DECCOLM into compat addon this._terminal.savedCols = this._bufferService.cols; this._terminal.resize(132, this._bufferService.rows); - this._terminal.reset(); + this._onRequestReset.fire(); break; case 6: - this._terminal.originMode = true; + this._coreService.decPrivateModes.origin = true; this._setCursor(0, 0); break; case 7: - this._terminal.wraparoundMode = true; + this._coreService.decPrivateModes.wraparound = true; break; case 12: // this.cursorBlink = true; break; case 66: this._logService.debug('Serial port requested application keypad.'); - this._terminal.applicationKeypad = true; - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._coreService.decPrivateModes.applicationKeypad = true; + this._terminal.viewport?.syncScrollArea(); break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. @@ -1461,19 +1609,17 @@ export class InputHandler extends Disposable implements IInputHandler { // focusout: ^[[O this._terminal.sendFocus = true; break; - case 1005: // utf8 ext mode mouse - // for wide terminals - // simply encodes large values as utf8 characters - this._coreMouseService.activeEncoding = 'UTF8'; + case 1005: // utf8 ext mode mouse - removed in #2507 + this._logService.debug('DECSET 1005 not supported (see #2507)'); break; case 1006: // sgr ext mode mouse this._coreMouseService.activeEncoding = 'SGR'; break; - case 1015: // urxvt ext mode mouse - this._coreMouseService.activeEncoding = 'URXVT'; + case 1015: // urxvt ext mode mouse - removed in #2507 + this._logService.debug('DECSET 1015 not supported (see #2507)'); break; case 25: // show cursor - this._terminal.cursorHidden = false; + this._coreService.isCursorHidden = false; break; case 1048: // alt screen cursor this.saveCursor(); @@ -1483,11 +1629,9 @@ export class InputHandler extends Disposable implements IInputHandler { // FALL-THROUGH case 47: // alt screen buffer case 1047: // alt screen buffer - this._bufferService.buffers.activateAltBuffer(this._terminal.eraseAttrData()); - this._terminal.refresh(0, this._bufferService.rows - 1); - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._bufferService.buffers.activateAltBuffer(this._eraseAttrData()); + this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); + this._terminal.viewport?.syncScrollArea(); this._terminal.showCursor(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) @@ -1606,24 +1750,22 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.resize(this._terminal.savedCols, this._bufferService.rows); } delete this._terminal.savedCols; - this._terminal.reset(); + this._onRequestReset.fire(); break; case 6: - this._terminal.originMode = false; + this._coreService.decPrivateModes.origin = false; this._setCursor(0, 0); break; case 7: - this._terminal.wraparoundMode = false; + this._coreService.decPrivateModes.wraparound = false; break; case 12: // this.cursorBlink = false; break; case 66: this._logService.debug('Switching back to normal keypad.'); - this._terminal.applicationKeypad = false; - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._coreService.decPrivateModes.applicationKeypad = false; + this._terminal.viewport?.syncScrollArea(); break; case 9: // X10 Mouse case 1000: // vt200 mouse @@ -1634,17 +1776,17 @@ export class InputHandler extends Disposable implements IInputHandler { case 1004: // send focusin/focusout events this._terminal.sendFocus = false; break; - case 1005: // utf8 ext mode mouse - this._coreMouseService.activeEncoding = 'DEFAULT'; + case 1005: // utf8 ext mode mouse - removed in #2507 + this._logService.debug('DECRST 1005 not supported (see #2507)'); break; case 1006: // sgr ext mode mouse this._coreMouseService.activeEncoding = 'DEFAULT'; break; - case 1015: // urxvt ext mode mouse - this._coreMouseService.activeEncoding = 'DEFAULT'; + case 1015: // urxvt ext mode mouse - removed in #2507 + this._logService.debug('DECRST 1015 not supported (see #2507)'); break; case 25: // hide cursor - this._terminal.cursorHidden = true; + this._coreService.isCursorHidden = true; break; case 1048: // alt screen cursor this.restoreCursor(); @@ -1658,10 +1800,8 @@ export class InputHandler extends Disposable implements IInputHandler { if (params.params[i] === 1049) { this.restoreCursor(); } - this._terminal.refresh(0, this._bufferService.rows - 1); - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); + this._terminal.viewport?.syncScrollArea(); this._terminal.showCursor(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) @@ -1811,14 +1951,14 @@ export class InputHandler extends Disposable implements IInputHandler { public charAttributes(params: IParams): void { // Optimize a single SGR0. if (params.length === 1 && params.params[0] === 0) { - this._terminal.curAttrData.fg = DEFAULT_ATTR_DATA.fg; - this._terminal.curAttrData.bg = DEFAULT_ATTR_DATA.bg; + this._curAttrData.fg = DEFAULT_ATTR_DATA.fg; + this._curAttrData.bg = DEFAULT_ATTR_DATA.bg; return; } const l = params.length; let p; - const attr = this._terminal.curAttrData; + const attr = this._curAttrData; for (let i = 0; i < l; i++) { p = params.params[i]; @@ -1978,22 +2118,15 @@ export class InputHandler extends Disposable implements IInputHandler { * http://vt100.net/docs/vt220-rm/table4-10.html */ public softReset(params: IParams): void { - this._terminal.cursorHidden = false; + this._coreService.isCursorHidden = false; this._terminal.insertMode = false; - this._terminal.originMode = false; - this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false - this._terminal.applicationKeypad = false; // ? - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } - this._coreService.decPrivateModes.applicationCursorKeys = false; + this._terminal.viewport?.syncScrollArea(); this._bufferService.buffer.scrollTop = 0; this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; - this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); + this._curAttrData = DEFAULT_ATTR_DATA.clone(); this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? - this._terminal.charset = null; - this._terminal.glevel = 0; // ?? - this._terminal.charsets = [null]; // ?? + this._coreService.reset(); + this._charsetService.reset(); } /** @@ -2055,9 +2188,9 @@ export class InputHandler extends Disposable implements IInputHandler { 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._terminal.curAttrData.fg; - this._bufferService.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg; - this._bufferService.buffer.savedCharset = this._terminal.charset; + this._bufferService.buffer.savedCurAttrData.fg = this._curAttrData.fg; + this._bufferService.buffer.savedCurAttrData.bg = this._curAttrData.bg; + this._bufferService.buffer.savedCharset = this._charsetService.charset; } @@ -2069,11 +2202,11 @@ export class InputHandler extends Disposable implements IInputHandler { 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._terminal.curAttrData.fg = this._bufferService.buffer.savedCurAttrData.fg; - this._terminal.curAttrData.bg = this._bufferService.buffer.savedCurAttrData.bg; - this._terminal.charset = (this as any)._savedCharset; + this._curAttrData.fg = this._bufferService.buffer.savedCurAttrData.fg; + this._curAttrData.bg = this._bufferService.buffer.savedCurAttrData.bg; + this._charsetService.charset = (this as any)._savedCharset; if (this._bufferService.buffer.savedCharset) { - this._terminal.charset = this._bufferService.buffer.savedCharset; + this._charsetService.charset = this._bufferService.buffer.savedCharset; } this._restrictCursor(); } @@ -2106,10 +2239,8 @@ export class InputHandler extends Disposable implements IInputHandler { */ public keypadApplicationMode(): void { this._logService.debug('Serial port requested application keypad.'); - this._terminal.applicationKeypad = true; - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._coreService.decPrivateModes.applicationKeypad = true; + this._terminal.viewport?.syncScrollArea(); } /** @@ -2119,10 +2250,8 @@ export class InputHandler extends Disposable implements IInputHandler { */ public keypadNumericMode(): void { this._logService.debug('Switching back to normal keypad.'); - this._terminal.applicationKeypad = false; - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._coreService.decPrivateModes.applicationKeypad = false; + this._terminal.viewport?.syncScrollArea(); } /** @@ -2132,8 +2261,8 @@ export class InputHandler extends Disposable implements IInputHandler { * therefore ESC % G does the same. */ public selectDefaultCharset(): void { - this._terminal.setgLevel(0); - this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default) + this._charsetService.setgLevel(0); + this._charsetService.setgCharset(0, DEFAULT_CHARSET); // US (default) } /** @@ -2160,7 +2289,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (collectAndFlag[0] === '/') { return; // TODO: Is this supported? } - this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); + this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); return; } @@ -2176,7 +2305,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(); + this._terminal.scroll(this._eraseAttrData()); } else if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } @@ -2210,7 +2339,7 @@ export class InputHandler extends Disposable implements IInputHandler { // blankLine(true) is xterm/linux behavior const scrollRegionHeight = buffer.scrollBottom - buffer.scrollTop; buffer.lines.shiftElements(buffer.y + buffer.ybase, scrollRegionHeight, 1); - buffer.lines.set(buffer.y + buffer.ybase, buffer.getBlankLine(this._terminal.eraseAttrData())); + buffer.lines.set(buffer.y + buffer.ybase, buffer.getBlankLine(this._eraseAttrData())); this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } else { buffer.y--; @@ -2223,9 +2352,23 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) * Reset to initial state. */ - public reset(): void { + public fullReset(): void { this._parser.reset(); - this._terminal.reset(); // TODO: save to move from terminal? + this._onRequestReset.fire(); + } + + public reset(): void { + this._curAttrData = DEFAULT_ATTR_DATA.clone(); + this._eraseAttrDataInternal = DEFAULT_ATTR_DATA.clone(); + } + + /** + * back_color_erase feature for xterm. + */ + private _eraseAttrData(): IAttributeData { + this._eraseAttrDataInternal.bg &= ~(Attributes.CM_MASK | 0xFFFFFF); + this._eraseAttrDataInternal.bg |= this._curAttrData.bg & ~0xFC000000; + return this._eraseAttrDataInternal; } /** @@ -2239,7 +2382,7 @@ export class InputHandler extends Disposable implements IInputHandler { * you use another locking shift. (partly supported) */ public setgLevel(level: number): void { - this._terminal.setgLevel(level); // TODO: save to move from terminal? + this._charsetService.setgLevel(level); } /** @@ -2254,8 +2397,8 @@ export class InputHandler extends Disposable implements IInputHandler { // prepare cell data const cell = new CellData(); cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0); - cell.fg = this._terminal.curAttrData.fg; - cell.bg = this._terminal.curAttrData.bg; + cell.fg = this._curAttrData.fg; + cell.bg = this._curAttrData.bg; const buffer = this._bufferService.buffer; diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 667bf0764..90522bf4f 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -112,7 +112,7 @@ describe('Terminal', () => { assert.equal(typeof e, 'number'); done(); }); - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); }); it('should fire the onTitleChange event', (done) => { term.onTitleChange(e => { @@ -163,17 +163,6 @@ describe('Terminal', () => { }); }); - describe('reset', () => { - it('should not affect cursorState', () => { - term.cursorState = 1; - term.reset(); - assert.equal(term.cursorState, 1); - term.cursorState = 0; - term.reset(); - assert.equal(term.cursorState, 0); - }); - }); - describe('clear', () => { it('should clear a buffer equal to rows', () => { const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); @@ -408,7 +397,7 @@ describe('Terminal', () => { term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b'); @@ -421,7 +410,7 @@ describe('Terminal', () => { term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); @@ -435,7 +424,7 @@ describe('Terminal', () => { term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b'); @@ -454,7 +443,7 @@ describe('Terminal', () => { term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); @@ -476,7 +465,7 @@ describe('Terminal', () => { term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line assert.equal(term.buffer.lines.length, INIT_ROWS); - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); // 'a' gets pushed out of buffer assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); @@ -491,7 +480,7 @@ describe('Terminal', () => { term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); @@ -505,7 +494,7 @@ describe('Terminal', () => { term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); @@ -523,7 +512,7 @@ describe('Terminal', () => { term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); @@ -758,7 +747,7 @@ describe('Terminal', () => { const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; - term.wraparoundMode = true; + 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)); @@ -772,7 +761,7 @@ describe('Terminal', () => { const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; - term.wraparoundMode = false; + term.writeSync('\x1b[?7l'); // Disable wraparound mode const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); if (width !== 1) { continue; @@ -823,7 +812,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(1); }); it('multiple combined é', () => { - term.wraparoundMode = true; term.writeSync(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); @@ -837,7 +825,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(1); }); it('multiple surrogate with combined', () => { - term.wraparoundMode = true; term.writeSync(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); @@ -866,7 +853,6 @@ describe('Terminal', () => { expect(term.buffer.x).eql(3); }); it('line of ¥ even', () => { - term.wraparoundMode = true; term.writeSync(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); @@ -886,7 +872,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of ¥ odd', () => { - term.wraparoundMode = true; term.buffer.x = 1; term.writeSync(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { @@ -911,7 +896,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of ¥ with combining odd', () => { - term.wraparoundMode = true; term.buffer.x = 1; term.writeSync(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { @@ -936,7 +920,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of ¥ with combining even', () => { - term.wraparoundMode = true; term.writeSync(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); @@ -956,7 +939,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of surrogate fullwidth with combining odd', () => { - term.wraparoundMode = true; term.buffer.x = 1; term.writeSync(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { @@ -981,7 +963,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of surrogate fullwidth with combining even', () => { - term.wraparoundMode = true; term.writeSync(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); diff --git a/src/Terminal.ts b/src/Terminal.ts index 63400bcb9..74c2d00ea 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -28,7 +28,7 @@ import { Viewport } from 'browser/Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; import { C0 } from 'common/data/EscapeSequences'; import { InputHandler } from './InputHandler'; -import { Renderer } from './renderer/Renderer'; +import { Renderer } from 'browser/renderer/Renderer'; import { Linkifier } from 'browser/Linkifier'; import { SelectionService } from 'browser/services/SelectionService'; import * as Browser from 'common/Platform'; @@ -38,22 +38,21 @@ import { SoundService } from 'browser/services/SoundService'; import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; -import { DomRenderer } from './renderer/dom/DomRenderer'; -import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; +import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; +import { IKeyboardEvent, KeyboardResultType, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { applyWindowsMode } from './WindowsMode'; +import { handleWindowsModeLineFeed } from 'common/WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; +import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService, ICharsetService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { Disposable } from 'common/Lifecycle'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; -import { Attributes } from 'common/buffer/Constants'; import { MouseService } from 'browser/services/MouseService'; import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; @@ -63,6 +62,8 @@ import { DirtyRowService } from 'common/services/DirtyRowService'; import { InstantiationService } from 'common/services/InstantiationService'; import { CoreMouseService } from 'common/services/CoreMouseService'; import { WriteBuffer } from 'common/input/WriteBuffer'; +import { CoreBrowserService } from 'browser/services/CoreBrowserService'; +import { CharsetService } from 'common/services/CharsetService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -73,10 +74,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public element: HTMLElement; public screenElement: HTMLElement; - /** - * The HTMLElement that the terminal is created in, set by Terminal.open. - */ - private _parent: HTMLElement; private _document: Document; private _viewportScrollArea: HTMLElement; private _viewportElement: HTMLElement; @@ -90,15 +87,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // TODO: We should remove options once components adopt optionsService public get options(): ITerminalOptions { return this.optionsService.options; } - // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options - public cursorState: number; - public cursorHidden: boolean; - private _customKeyEventHandler: CustomKeyEventHandler; // common services private _bufferService: IBufferService; private _coreService: ICoreService; + private _charsetService: ICharsetService; private _coreMouseService: ICoreMouseService; private _dirtyRowService: IDirtyRowService; private _instantiationService: IInstantiationService; @@ -113,19 +107,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _soundService: ISoundService; // modes - public applicationKeypad: boolean; - public originMode: boolean; public insertMode: boolean; - public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false public bracketedPasteMode: boolean; - // charset - // The current charset - public charset: ICharset; - public gcharset: number; - public glevel: number; - public charsets: ICharset[]; - // mouse properties public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE; public sendFocus: boolean; @@ -133,12 +117,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // misc public savedCols: number; - public curAttrData: IAttributeData; - private _eraseAttrData: IAttributeData; - - public params: (string | number)[]; - public currentParam: string | number; - // write buffer private _writeBuffer: WriteBuffer; @@ -172,6 +150,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onData = new EventEmitter(); public get onData(): IEvent { return this._onData.event; } + private _onBinary = new EventEmitter(); + public get onBinary(): IEvent { return this._onBinary.event; } private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } private _onLineFeed = new EventEmitter(); @@ -224,10 +204,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); this._instantiationService.setService(ICoreService, this._coreService); this._coreService.onData(e => this._onData.fire(e)); + this._coreService.onBinary(e => this._onBinary.fire(e)); 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._charsetService = this._instantiationService.createInstance(CharsetService); + this._instantiationService.setService(ICharsetService, this._charsetService); this._setupOptionsListeners(); this._setup(); @@ -240,59 +223,48 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return; } super.dispose(); - if (this._windowsMode) { - this._windowsMode.dispose(); - this._windowsMode = undefined; - } - if (this._renderService) { - this._renderService.dispose(); - } + this._windowsMode?.dispose(); + this._windowsMode = undefined; + this._renderService?.dispose(); this._customKeyEventHandler = null; this.write = () => {}; - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } + this.element?.parentNode?.removeChild(this.element); } private _setup(): void { - this._parent = document ? document.body : null; - - this.cursorState = 0; - this.cursorHidden = false; this._customKeyEventHandler = null; // modes - this.applicationKeypad = false; - this.originMode = false; this.insertMode = false; - this.wraparoundMode = true; // defaults: xterm - true, vt100 - false this.bracketedPasteMode = false; - // charset - this.charset = null; - this.gcharset = null; - this.glevel = 0; - // TODO: Can this be just []? - this.charsets = [null]; - - this.curAttrData = DEFAULT_ATTR_DATA.clone(); - this._eraseAttrData = DEFAULT_ATTR_DATA.clone(); - - this.params = []; - this.currentParam = 0; - this._userScrolling = false; - // Register input handler and refire/handle events - this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); - this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); - this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); - this.register(this._inputHandler); + if (this._inputHandler) { + this._inputHandler.reset(); + } else { + // Register input handler and refire/handle events + this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); + this._inputHandler.onRequestBell(() => this.bell()); + this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); + this._inputHandler.onRequestReset(() => this.reset()); + this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); + this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); + this.register(this._inputHandler); + } - this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); + if (!this.linkifier) { + this.linkifier = new Linkifier(this._bufferService, this._logService); + } if (this.options.windowsMode) { - this._windowsMode = applyWindowsMode(this); + this._enableWindowsMode(); + } + } + + private _enableWindowsMode(): void { + if (!this._windowsMode) { + this._windowsMode = this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService)); } } @@ -307,15 +279,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return this._bufferService.buffers; } - /** - * back_color_erase feature for xterm. - */ - public eraseAttrData(): IAttributeData { - this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF); - this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000; - return this._eraseAttrData; - } - /** * Focus the terminal. Delegates focus handling to the terminal's DOM element. */ @@ -325,10 +288,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } - public get isFocused(): boolean { - return document.activeElement === this.textarea && document.hasFocus(); - } - private _setupOptionsListeners(): void { // TODO: These listeners should be owned by individual components this.optionsService.onOptionChange(key => { @@ -336,18 +295,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp case 'fontFamily': case 'fontSize': // When the font changes the size of the cells may change which requires a renderer clear - if (this._renderService) { - this._renderService.clear(); - } - if (this._charSizeService) { - this._charSizeService.measure(); - } + this._renderService?.clear(); + this._charSizeService?.measure(); break; case 'drawBoldTextInBrightColors': case 'letterSpacing': case 'lineHeight': case 'fontWeight': case 'fontWeightBold': + case 'minimumContrastRatio': // When the font changes the size of the cells may change which requires a renderer clear if (this._renderService) { this._renderService.clear(); @@ -363,9 +319,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp break; case 'scrollback': this.buffers.resize(this.cols, this.rows); - if (this.viewport) { - this.viewport.syncScrollArea(); - } + this.viewport?.syncScrollArea(); break; case 'screenReaderMode': if (this.optionsService.options.screenReaderMode) { @@ -373,38 +327,20 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._accessibilityManager = new AccessibilityManager(this, this._renderService); } } else { - if (this._accessibilityManager) { - this._accessibilityManager.dispose(); - this._accessibilityManager = null; - } + this._accessibilityManager?.dispose(); + this._accessibilityManager = null; } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; case 'theme': this._setTheme(this.optionsService.options.theme); break; - case 'scrollback': - const newBufferLength = this.rows + this.optionsService.options.scrollback; - if (this.buffer.lines.length > newBufferLength) { - const amountToTrim = this.buffer.lines.length - newBufferLength; - const needsRefresh = (this.buffer.ydisp - amountToTrim < 0); - this.buffer.lines.trimStart(amountToTrim); - this.buffer.ybase = Math.max(this.buffer.ybase - amountToTrim, 0); - this.buffer.ydisp = Math.max(this.buffer.ydisp - amountToTrim, 0); - if (needsRefresh) { - this.refresh(0, this.rows - 1); - } - } case 'windowsMode': if (this.optionsService.options.windowsMode) { - if (!this._windowsMode) { - this._windowsMode = applyWindowsMode(this); - } + this._enableWindowsMode(); } else { - if (this._windowsMode) { - this._windowsMode.dispose(); - this._windowsMode = undefined; - } + this._windowsMode?.dispose(); + this._windowsMode = undefined; } break; } @@ -514,13 +450,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * @param parent The element to create the terminal within. */ public open(parent: HTMLElement): void { - this._parent = parent || this._parent; - - if (!this._parent) { + if (!parent) { throw new Error('Terminal requires a parent element.'); } - this._document = this._parent.ownerDocument; + if (!document.body.contains(parent)) { + this._logService.warn('Terminal.open was called on an element that was not attached to the DOM'); + } + + this._document = parent.ownerDocument; // Create main element container this.element = this._document.createElement('div'); @@ -528,7 +466,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.element.classList.add('terminal'); this.element.classList.add('xterm'); this.element.setAttribute('tabindex', '0'); - this._parent.appendChild(this.element); + parent.appendChild(this.element); // Performance: Use a document fragment to build the terminal // viewport and helper elements detached from the DOM @@ -561,6 +499,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this._helperContainer.appendChild(this.textarea); + const coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea); + this._instantiationService.setService(ICoreBrowserService, coreBrowserService); + this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); this._instantiationService.setService(ICharSizeService, this._charSizeService); @@ -575,6 +516,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._theme = this.options.theme || this._theme; this.options.theme = undefined; this._colorManager = new ColorManager(document, this.options.allowTransparency); + this.optionsService.onOptionChange(e => this._colorManager.onOptionsChange(e)); this._colorManager.setTheme(this._theme); const renderer = this._createRenderer(); @@ -661,8 +603,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _createRenderer(): IRenderer { switch (this.options.rendererType) { - case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService, this.optionsService); - case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService); + case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager.colors, this.screenElement, this.linkifier); + case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager.colors, this.element, this.screenElement, this._viewportElement, this.linkifier); default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } } @@ -673,15 +615,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp */ private _setTheme(theme: ITheme): void { this._theme = theme; - if (this._colorManager) { - this._colorManager.setTheme(theme); - } - if (this._renderService) { - this._renderService.setColors(this._colorManager.colors); - } - if (this.viewport) { - this.viewport.onThemeChange(this._colorManager.colors); - } + this._colorManager?.setTheme(theme); + this._renderService?.setColors(this._colorManager.colors); + this.viewport?.onThemeChange(this._colorManager.colors); } /** @@ -948,9 +884,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * @param end The row to end at (between start and this.rows - 1). */ public refresh(start: number, end: number): void { - if (this._renderService) { - this._renderService.refreshRows(start, end); - } + this._renderService?.refreshRows(start, end); } /** @@ -959,9 +893,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * @param end The row to end at (between start and this.rows - 1). */ private _queueLinkification(start: number, end: number): void { - if (this.linkifier) { - this.linkifier.linkifyRows(start, end); - } + this.linkifier?.linkifyRows(start, end); } /** @@ -979,8 +911,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Display the cursor element */ public showCursor(): void { - if (!this.cursorState) { - this.cursorState = 1; + if (!this._coreService.isCursorInitialized) { + this._coreService.isCursorInitialized = true; this.refresh(this.buffer.y, this.buffer.y); } } @@ -989,10 +921,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Scroll the terminal down 1 row, creating a blank line. * @param isWrapped Whether the new line is wrapped from the previous line. */ - public scroll(isWrapped: boolean = false): void { + public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { let newLine: IBufferLine; newLine = this._blankLine; - const eraseAttr = this.eraseAttrData(); if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { newLine = this.buffer.getBlankLine(eraseAttr, isWrapped); this._blankLine = newLine; @@ -1241,24 +1172,18 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Clears the current terminal selection. */ public clearSelection(): void { - if (this._selectionService) { - this._selectionService.clearSelection(); - } + this._selectionService?.clearSelection(); } /** * Selects all text within the terminal. */ public selectAll(): void { - if (this._selectionService) { - this._selectionService.selectAll(); - } + this._selectionService?.selectAll(); } public selectLines(start: number, end: number): void { - if (this._selectionService) { - this._selectionService.selectLines(start, end); - } + this._selectionService?.selectLines(start, end); } /** @@ -1343,27 +1268,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); } - /** - * Set the G level of the terminal - * @param g - */ - public setgLevel(g: number): void { - this.glevel = g; - this.charset = this.charsets[g]; - } - - /** - * Set the charset for the given G level of the terminal - * @param g - * @param charset - */ - public setgCharset(g: number, charset: ICharset): void { - this.charsets[g] = charset; - if (this.glevel === g) { - this.charset = charset; - } - } - protected _keyUp(ev: KeyboardEvent): void { if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { return; @@ -1465,9 +1369,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._bufferService.resize(x, y); this.buffers.setupTabStops(this.cols); - if (this._charSizeService) { - this._charSizeService.measure(); - } + this._charSizeService?.measure(); + + // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an + // invalid location + this.viewport.syncScrollArea(true); this.refresh(0, this.rows - 1); this._onResize.fire({ cols: x, rows: y }); @@ -1547,29 +1453,22 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.options.rows = this.rows; this.options.cols = this.cols; const customKeyEventHandler = this._customKeyEventHandler; - const inputHandler = this._inputHandler; - const cursorState = this.cursorState; const userScrolling = this._userScrolling; this._setup(); this._bufferService.reset(); + this._charsetService.reset(); this._coreService.reset(); this._coreMouseService.reset(); - if (this._selectionService) { - this._selectionService.reset(); - } + this._selectionService?.reset(); // reattach this._customKeyEventHandler = customKeyEventHandler; - this._inputHandler = inputHandler; - this.cursorState = cursorState; this._userScrolling = userScrolling; // do a full screen refresh this.refresh(0, this.rows - 1); - if (this.viewport) { - this.viewport.syncScrollArea(); - } + this.viewport?.syncScrollArea(); } // TODO: Remove cancel function and cancelEvents option diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index efb2a4462..f7046240b 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -34,7 +34,6 @@ if (os.platform() === 'darwin') { // filter skipFilenames const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slice(-1)[0]) === -1); - describe('Escape Sequence Files', function(): void { this.timeout(1000); @@ -44,6 +43,9 @@ describe('Escape Sequence Files', function(): void { let customHandler: IDisposable | undefined; before(() => { + if (process.platform === 'win32') { + return; + } ptyTerm = (pty as any).open({cols: COLS, rows: ROWS}); slaveEnd = ptyTerm._slave; term = new Terminal({cols: COLS, rows: ROWS}); @@ -51,12 +53,15 @@ describe('Escape Sequence Files', function(): void { }); after(() => { + if (process.platform === 'win32') { + return; + } ptyTerm._master.end(); ptyTerm._master.destroy(); }); FILES.forEach(filename => { - it(filename.split('/').slice(-1)[0], async () => { + (process.platform === 'win32' ? it.skip : it)(filename.split('/').slice(-1)[0], async () => { // reset terminal and handler if (customHandler) { customHandler.dispose(); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index a70f751c5..57a9fe785 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; -import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types'; +import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; @@ -19,6 +19,7 @@ import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { + get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } keyDown(ev: any): boolean { return this._keyDown(ev); } keyPress(ev: any): boolean { return this._keyPress(ev); } } @@ -32,6 +33,7 @@ export class MockTerminal implements ITerminal { onLineFeed: IEvent; onSelectionChange: IEvent; onData: IEvent; + onBinary: IEvent; onTitleChange: IEvent; onScroll: IEvent; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; @@ -196,112 +198,25 @@ export class MockTerminal implements ITerminal { export class MockInputHandlingTerminal implements IInputHandlingTerminal { onA11yCharEmitter: EventEmitter; onA11yTabEmitter: EventEmitter; - element: HTMLElement; - options: ITerminalOptions = {}; - cols: number; - rows: number; - charset: { [key: string]: string; }; - gcharset: number; - glevel: number; - charsets: { [key: string]: string; }[]; - applicationKeypad: boolean; - applicationCursor: boolean; - originMode: boolean; insertMode: boolean; - wraparoundMode: boolean; bracketedPasteMode: boolean; - curAttrData = new AttributeData(); savedCols: number; - x10Mouse: boolean; - vt200Mouse: boolean; - normalMouse: boolean; - mouseEvents: CoreMouseEventType; sendFocus: boolean; - utfMouse: boolean; - sgrMouse: boolean; - urxvtMouse: boolean; - cursorHidden: boolean; buffers: IBufferSet; buffer: IBuffer = new MockBuffer(); viewport: IViewport; - selectionService: ISelectionService; - focus(): void { - throw new Error('Method not implemented.'); - } - convertEol: boolean; - bell(): void { - throw new Error('Method not implemented.'); - } - - updateRange(y: number): void { - throw new Error('Method not implemented.'); - } - scroll(isWrapped?: boolean): void { - throw new Error('Method not implemented.'); - } - nextStop(x?: number): number { - throw new Error('Method not implemented.'); - } - setgLevel(g: number): void { - throw new Error('Method not implemented.'); - } - eraseAttrData(): IAttributeData { - throw new Error('Method not implemented.'); - } - eraseRight(x: number, y: number): void { - throw new Error('Method not implemented.'); - } - eraseLine(y: number): void { - throw new Error('Method not implemented.'); - } - eraseLeft(x: number, y: number): void { - throw new Error('Method not implemented.'); - } - prevStop(x?: number): number { + scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void { throw new Error('Method not implemented.'); } is(term: string): boolean { throw new Error('Method not implemented.'); } - setgCharset(g: number, charset: { [key: string]: string; }): void { - throw new Error('Method not implemented.'); - } resize(x: number, y: number): void { throw new Error('Method not implemented.'); } - log(text: string, data?: any): void { - throw new Error('Method not implemented.'); - } - reset(): void { - throw new Error('Method not implemented.'); - } showCursor(): void { throw new Error('Method not implemented.'); } - refresh(start: number, end: number): void { - throw new Error('Method not implemented.'); - } - matchColor(r1: number, g1: number, b1: number): number { - throw new Error('Method not implemented.'); - } - error(text: string, data?: any): void { - throw new Error('Method not implemented.'); - } - setOption(key: string, value: any): void { - (this.options)[key] = value; - } - on(type: string, listener: XtermListener): void { - throw new Error('Method not implemented.'); - } - off(type: string, listener: XtermListener): void { - throw new Error('Method not implemented.'); - } - emit(type: string, data?: any): void { - throw new Error('Method not implemented.'); - } - addDisposableListener(type: string, handler: XtermListener): IDisposable { - throw new Error('Method not implemented.'); - } handler(data: string): void { throw new Error('Method not implemented.'); } @@ -362,6 +277,7 @@ export class MockBuffer implements IBuffer { } export class MockRenderer implements IRenderer { + onRequestRefreshRows: IEvent; onCanvasResize: IEvent<{ width: number; height: number; }>; onRender: IEvent<{ start: number; end: number; }>; dispose(): void { diff --git a/src/Types.d.ts b/src/Types.d.ts index cb45d3b1f..ebc5d338c 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -21,24 +21,10 @@ export type LineData = CharData[]; * InputHandler cleanly from the ITerminal interface. */ export interface IInputHandlingTerminal { - element: HTMLElement; - options: ITerminalOptions; - cols: number; - rows: number; - charset: ICharset; - gcharset: number; - glevel: number; - charsets: ICharset[]; - applicationKeypad: boolean; - originMode: boolean; insertMode: boolean; - wraparoundMode: boolean; bracketedPasteMode: boolean; - curAttrData: IAttributeData; savedCols: number; - mouseEvents: CoreMouseEventType; sendFocus: boolean; - cursorHidden: boolean; buffers: IBufferSet; buffer: IBuffer; @@ -47,17 +33,10 @@ export interface IInputHandlingTerminal { onA11yCharEmitter: IEventEmitter; onA11yTabEmitter: IEventEmitter; - bell(): void; - focus(): void; - scroll(isWrapped?: boolean): void; - setgLevel(g: number): void; - eraseAttrData(): IAttributeData; + scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; is(term: string): boolean; - setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; - reset(): void; showCursor(): void; - refresh(start: number, end: number): void; handleTitle(title: string): void; } @@ -85,7 +64,9 @@ export interface IInputHandler { /** C0 SI */ shiftIn(): void; /** CSI @ */ insertChars(params: IParams): void; + /** CSI SP @ */ scrollLeft(params: IParams): void; /** CSI A */ cursorUp(params: IParams): void; + /** CSI SP A */ scrollRight(params: IParams): void; /** CSI B */ cursorDown(params: IParams): void; /** CSI C */ cursorForward(params: IParams): void; /** CSI D */ cursorBackward(params: IParams): void; @@ -121,6 +102,8 @@ export interface IInputHandler { /** CSI r */ setScrollRegion(params: IParams, collect?: string): void; /** CSI s */ saveCursor(params: IParams): void; /** CSI u */ restoreCursor(params: IParams): void; + /** CSI ' } */ insertColumns(params: IParams): void; + /** CSI ' ~ */ deleteColumns(params: IParams): void; /** OSC 0 OSC 2 */ setTitle(data: string): void; /** ESC E */ nextLine(): void; @@ -138,7 +121,7 @@ export interface IInputHandler { /** ESC D */ index(): void; /** ESC H */ tabSet(): void; /** ESC M */ reverseIndex(): void; - /** ESC c */ reset(): void; + /** ESC c */ fullReset(): void; /** ESC n ESC o ESC | @@ -150,11 +133,8 @@ export interface IInputHandler { export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { screenElement: HTMLElement; browser: IBrowser; - cursorHidden: boolean; - cursorState: number; buffer: IBuffer; buffers: IBufferSet; - isFocused: boolean; viewport: IViewport; bracketedPasteMode: boolean; optionsService: IOptionsService; @@ -173,13 +153,14 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc // Portions of the public API that are required by the internal Terminal export interface IPublicTerminal extends IDisposable { - textarea: HTMLTextAreaElement; + textarea: HTMLTextAreaElement | undefined; rows: number; cols: number; buffer: IBuffer; markers: IMarker[]; onCursorMove: IEvent; onData: IEvent; + onBinary: IEvent; onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; onLineFeed: IEvent; onScroll: IEvent; @@ -226,7 +207,7 @@ export interface IBufferAccessor { } export interface IElementAccessor { - readonly element: HTMLElement; + readonly element: HTMLElement | undefined; } export interface ILinkifierAccessor { diff --git a/src/browser/Clipboard.ts b/src/browser/Clipboard.ts index 594e4429e..a7c48bfb0 100644 --- a/src/browser/Clipboard.ts +++ b/src/browser/Clipboard.ts @@ -82,12 +82,12 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA // Reset the terminal textarea's styling // Timeout needs to be long enough for click event to be handled. setTimeout(() => { - textarea.style.position = null; - textarea.style.width = null; - textarea.style.height = null; - textarea.style.left = null; - textarea.style.top = null; - textarea.style.zIndex = null; + textarea.style.position = ''; + textarea.style.width = ''; + textarea.style.height = ''; + textarea.style.left = ''; + textarea.style.top = ''; + textarea.style.zIndex = ''; }, 200); } diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts new file mode 100644 index 000000000..ff8da246f --- /dev/null +++ b/src/browser/Color.test.ts @@ -0,0 +1,280 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'browser/Color'; + +describe('Color', () => { + + describe('channels', () => { + describe('toCss', () => { + it('should convert an rgb array to css hex string', () => { + assert.equal(channels.toCss(0x00, 0x00, 0x00), '#000000'); + assert.equal(channels.toCss(0x10, 0x10, 0x10), '#101010'); + assert.equal(channels.toCss(0x20, 0x20, 0x20), '#202020'); + assert.equal(channels.toCss(0x30, 0x30, 0x30), '#303030'); + assert.equal(channels.toCss(0x40, 0x40, 0x40), '#404040'); + assert.equal(channels.toCss(0x50, 0x50, 0x50), '#505050'); + assert.equal(channels.toCss(0x60, 0x60, 0x60), '#606060'); + assert.equal(channels.toCss(0x70, 0x70, 0x70), '#707070'); + assert.equal(channels.toCss(0x80, 0x80, 0x80), '#808080'); + assert.equal(channels.toCss(0x90, 0x90, 0x90), '#909090'); + assert.equal(channels.toCss(0xa0, 0xa0, 0xa0), '#a0a0a0'); + assert.equal(channels.toCss(0xb0, 0xb0, 0xb0), '#b0b0b0'); + assert.equal(channels.toCss(0xc0, 0xc0, 0xc0), '#c0c0c0'); + assert.equal(channels.toCss(0xd0, 0xd0, 0xd0), '#d0d0d0'); + assert.equal(channels.toCss(0xe0, 0xe0, 0xe0), '#e0e0e0'); + assert.equal(channels.toCss(0xf0, 0xf0, 0xf0), '#f0f0f0'); + assert.equal(channels.toCss(0xff, 0xff, 0xff), '#ffffff'); + }); + }); + + describe('toRgba', () => { + it('should convert an rgb array to an rgba number', () => { + assert.equal(channels.toRgba(0x00, 0x00, 0x00), 0x000000FF); + assert.equal(channels.toRgba(0x10, 0x10, 0x10), 0x101010FF); + assert.equal(channels.toRgba(0x20, 0x20, 0x20), 0x202020FF); + assert.equal(channels.toRgba(0x30, 0x30, 0x30), 0x303030FF); + assert.equal(channels.toRgba(0x40, 0x40, 0x40), 0x404040FF); + assert.equal(channels.toRgba(0x50, 0x50, 0x50), 0x505050FF); + assert.equal(channels.toRgba(0x60, 0x60, 0x60), 0x606060FF); + assert.equal(channels.toRgba(0x70, 0x70, 0x70), 0x707070FF); + assert.equal(channels.toRgba(0x80, 0x80, 0x80), 0x808080FF); + assert.equal(channels.toRgba(0x90, 0x90, 0x90), 0x909090FF); + assert.equal(channels.toRgba(0xa0, 0xa0, 0xa0), 0xa0a0a0FF); + assert.equal(channels.toRgba(0xb0, 0xb0, 0xb0), 0xb0b0b0FF); + assert.equal(channels.toRgba(0xc0, 0xc0, 0xc0), 0xc0c0c0FF); + assert.equal(channels.toRgba(0xd0, 0xd0, 0xd0), 0xd0d0d0FF); + assert.equal(channels.toRgba(0xe0, 0xe0, 0xe0), 0xe0e0e0FF); + assert.equal(channels.toRgba(0xf0, 0xf0, 0xf0), 0xf0f0f0FF); + assert.equal(channels.toRgba(0xff, 0xff, 0xff), 0xffffffFF); + }); + it('should convert an rgba array to an rgba number', () => { + assert.equal(channels.toRgba(0x00, 0x00, 0x00, 0x00), 0x00000000); + assert.equal(channels.toRgba(0x10, 0x10, 0x10, 0x10), 0x10101010); + assert.equal(channels.toRgba(0x20, 0x20, 0x20, 0x20), 0x20202020); + assert.equal(channels.toRgba(0x30, 0x30, 0x30, 0x30), 0x30303030); + assert.equal(channels.toRgba(0x40, 0x40, 0x40, 0x40), 0x40404040); + assert.equal(channels.toRgba(0x50, 0x50, 0x50, 0x50), 0x50505050); + assert.equal(channels.toRgba(0x60, 0x60, 0x60, 0x60), 0x60606060); + assert.equal(channels.toRgba(0x70, 0x70, 0x70, 0x70), 0x70707070); + assert.equal(channels.toRgba(0x80, 0x80, 0x80, 0x80), 0x80808080); + assert.equal(channels.toRgba(0x90, 0x90, 0x90, 0x90), 0x90909090); + assert.equal(channels.toRgba(0xa0, 0xa0, 0xa0, 0xa0), 0xa0a0a0a0); + assert.equal(channels.toRgba(0xb0, 0xb0, 0xb0, 0xb0), 0xb0b0b0b0); + assert.equal(channels.toRgba(0xc0, 0xc0, 0xc0, 0xc0), 0xc0c0c0c0); + assert.equal(channels.toRgba(0xd0, 0xd0, 0xd0, 0xd0), 0xd0d0d0d0); + assert.equal(channels.toRgba(0xe0, 0xe0, 0xe0, 0xe0), 0xe0e0e0e0); + assert.equal(channels.toRgba(0xf0, 0xf0, 0xf0, 0xf0), 0xf0f0f0f0); + assert.equal(channels.toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff); + }); + }); + }); + + describe('color', () => { + describe('blend', () => { + it('should blend colors based on the alpha channel', () => { + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF00', rgba: 0xFFFFFF00 }), { css: '#000000', rgba: 0x000000FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF10', rgba: 0xFFFFFF10 }), { css: '#101010', rgba: 0x101010FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF20', rgba: 0xFFFFFF20 }), { css: '#202020', rgba: 0x202020FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF30', rgba: 0xFFFFFF30 }), { css: '#303030', rgba: 0x303030FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF40', rgba: 0xFFFFFF40 }), { css: '#404040', rgba: 0x404040FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF50', rgba: 0xFFFFFF50 }), { css: '#505050', rgba: 0x505050FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF60', rgba: 0xFFFFFF60 }), { css: '#606060', rgba: 0x606060FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF70', rgba: 0xFFFFFF70 }), { css: '#707070', rgba: 0x707070FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF80', rgba: 0xFFFFFF80 }), { css: '#808080', rgba: 0x808080FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF90', rgba: 0xFFFFFF90 }), { css: '#909090', rgba: 0x909090FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFA0', rgba: 0xFFFFFFA0 }), { css: '#a0a0a0', rgba: 0xA0A0A0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFB0', rgba: 0xFFFFFFB0 }), { css: '#b0b0b0', rgba: 0xB0B0B0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFC0', rgba: 0xFFFFFFC0 }), { css: '#c0c0c0', rgba: 0xC0C0C0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFD0', rgba: 0xFFFFFFD0 }), { css: '#d0d0d0', rgba: 0xD0D0D0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFE0', rgba: 0xFFFFFFE0 }), { css: '#e0e0e0', rgba: 0xE0E0E0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFF0', rgba: 0xFFFFFFF0 }), { css: '#f0f0f0', rgba: 0xF0F0F0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }), { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }); + }); + }); + + describe('opaque', () => { + it('should make the color opaque', () => { + assert.deepEqual(color.opaque({ css: '#00000000', rgba: 0x00000000 }), { css: '#000000', rgba: 0x000000FF }); + assert.deepEqual(color.opaque({ css: '#10101010', rgba: 0x10101010 }), { css: '#101010', rgba: 0x101010FF }); + assert.deepEqual(color.opaque({ css: '#20202020', rgba: 0x20202020 }), { css: '#202020', rgba: 0x202020FF }); + assert.deepEqual(color.opaque({ css: '#30303030', rgba: 0x30303030 }), { css: '#303030', rgba: 0x303030FF }); + assert.deepEqual(color.opaque({ css: '#40404040', rgba: 0x40404040 }), { css: '#404040', rgba: 0x404040FF }); + assert.deepEqual(color.opaque({ css: '#50505050', rgba: 0x50505050 }), { css: '#505050', rgba: 0x505050FF }); + assert.deepEqual(color.opaque({ css: '#60606060', rgba: 0x60606060 }), { css: '#606060', rgba: 0x606060FF }); + assert.deepEqual(color.opaque({ css: '#70707070', rgba: 0x70707070 }), { css: '#707070', rgba: 0x707070FF }); + assert.deepEqual(color.opaque({ css: '#80808080', rgba: 0x80808080 }), { css: '#808080', rgba: 0x808080FF }); + assert.deepEqual(color.opaque({ css: '#90909090', rgba: 0x90909090 }), { css: '#909090', rgba: 0x909090FF }); + assert.deepEqual(color.opaque({ css: '#a0a0a0a0', rgba: 0xa0a0a0a0 }), { css: '#a0a0a0', rgba: 0xa0a0a0FF }); + assert.deepEqual(color.opaque({ css: '#b0b0b0b0', rgba: 0xb0b0b0b0 }), { css: '#b0b0b0', rgba: 0xb0b0b0FF }); + assert.deepEqual(color.opaque({ css: '#c0c0c0c0', rgba: 0xc0c0c0c0 }), { css: '#c0c0c0', rgba: 0xc0c0c0FF }); + assert.deepEqual(color.opaque({ css: '#d0d0d0d0', rgba: 0xd0d0d0d0 }), { css: '#d0d0d0', rgba: 0xd0d0d0FF }); + assert.deepEqual(color.opaque({ css: '#e0e0e0e0', rgba: 0xe0e0e0e0 }), { css: '#e0e0e0', rgba: 0xe0e0e0FF }); + assert.deepEqual(color.opaque({ css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }), { css: '#f0f0f0', rgba: 0xf0f0f0FF }); + assert.deepEqual(color.opaque({ css: '#ffffffff', rgba: 0xffffffff }), { css: '#ffffff', rgba: 0xffffffFF }); + }); + }); + }); + + describe('css', () => { + describe('toColor', () => { + it('should covert a CSS string to an IColor', () => { + assert.deepEqual(css.toColor('#000000'), { css: '#000000', rgba: 0x000000FF }); + assert.deepEqual(css.toColor('#101010'), { css: '#101010', rgba: 0x101010FF }); + assert.deepEqual(css.toColor('#202020'), { css: '#202020', rgba: 0x202020FF }); + assert.deepEqual(css.toColor('#303030'), { css: '#303030', rgba: 0x303030FF }); + assert.deepEqual(css.toColor('#404040'), { css: '#404040', rgba: 0x404040FF }); + assert.deepEqual(css.toColor('#505050'), { css: '#505050', rgba: 0x505050FF }); + assert.deepEqual(css.toColor('#606060'), { css: '#606060', rgba: 0x606060FF }); + assert.deepEqual(css.toColor('#707070'), { css: '#707070', rgba: 0x707070FF }); + assert.deepEqual(css.toColor('#808080'), { css: '#808080', rgba: 0x808080FF }); + assert.deepEqual(css.toColor('#909090'), { css: '#909090', rgba: 0x909090FF }); + assert.deepEqual(css.toColor('#a0a0a0'), { css: '#a0a0a0', rgba: 0xa0a0a0FF }); + assert.deepEqual(css.toColor('#b0b0b0'), { css: '#b0b0b0', rgba: 0xb0b0b0FF }); + assert.deepEqual(css.toColor('#c0c0c0'), { css: '#c0c0c0', rgba: 0xc0c0c0FF }); + assert.deepEqual(css.toColor('#d0d0d0'), { css: '#d0d0d0', rgba: 0xd0d0d0FF }); + assert.deepEqual(css.toColor('#e0e0e0'), { css: '#e0e0e0', rgba: 0xe0e0e0FF }); + assert.deepEqual(css.toColor('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF }); + assert.deepEqual(css.toColor('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF }); + }); + }); + }); + + describe('rgb', () => { + describe('relativeLuminance', () => { + it('should calculate the relative luminance of the color', () => { + assert.equal(rgb.relativeLuminance(0x000000), 0); + assert.equal(rgb.relativeLuminance(0x101010).toFixed(4), '0.0052'); + assert.equal(rgb.relativeLuminance(0x202020).toFixed(4), '0.0144'); + assert.equal(rgb.relativeLuminance(0x303030).toFixed(4), '0.0296'); + assert.equal(rgb.relativeLuminance(0x404040).toFixed(4), '0.0513'); + assert.equal(rgb.relativeLuminance(0x505050).toFixed(4), '0.0802'); + assert.equal(rgb.relativeLuminance(0x606060).toFixed(4), '0.1170'); + assert.equal(rgb.relativeLuminance(0x707070).toFixed(4), '0.1620'); + assert.equal(rgb.relativeLuminance(0x808080).toFixed(4), '0.2159'); + assert.equal(rgb.relativeLuminance(0x909090).toFixed(4), '0.2789'); + assert.equal(rgb.relativeLuminance(0xA0A0A0).toFixed(4), '0.3515'); + assert.equal(rgb.relativeLuminance(0xB0B0B0).toFixed(4), '0.4342'); + assert.equal(rgb.relativeLuminance(0xC0C0C0).toFixed(4), '0.5271'); + assert.equal(rgb.relativeLuminance(0xD0D0D0).toFixed(4), '0.6308'); + assert.equal(rgb.relativeLuminance(0xE0E0E0).toFixed(4), '0.7454'); + assert.equal(rgb.relativeLuminance(0xF0F0F0).toFixed(4), '0.8714'); + assert.equal(rgb.relativeLuminance(0xFFFFFF), 1); + }); + }); + }); + + describe('rgba', () => { + describe('ensureContrastRatio', () => { + it('should return undefined if the color already meets the contrast ratio (black bg)', () => { + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 1), undefined); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 2), undefined); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 3), undefined); + }); + it('should return a color that meets the contrast ratio (black bg)', () => { + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 4), 0x707070ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 5), 0x7f7f7fff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 6), 0x8c8c8cff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 7), 0x989898ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 8), 0xa3a3a3ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 9), 0xadadadff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 10), 0xb6b6b6ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 11), 0xbebebeff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 12), 0xc5c5c5ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 13), 0xd1d1d1ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 14), 0xd6d6d6ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 15), 0xdbdbdbff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 16), 0xe3e3e3ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 17), 0xe9e9e9ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 18), 0xeeeeeeff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 19), 0xf4f4f4ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 20), 0xfafafaff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 21), 0xffffffff); + }); + it('should return undefined if the color already meets the contrast ratio (white bg)', () => { + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 1), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 2), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 3), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 4), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 5), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 6), undefined); + }); + it('should return a color that meets the contrast ratio (white bg)', () => { + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 7), 0x565656ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 8), 0x4d4d4dff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 9), 0x454545ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 10), 0x3e3e3eff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 11), 0x373737ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 12), 0x313131ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 13), 0x313131ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 14), 0x272727ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 15), 0x232323ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 16), 0x1f1f1fff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 17), 0x1b1b1bff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 18), 0x151515ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 19), 0x101010ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 20), 0x080808ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 21), 0x000000ff); + }); + }); + + describe('toChannels', () => { + it('should convert an rgba number to an rgba array', () => { + assert.deepEqual(rgba.toChannels(0x00000000), [0x00, 0x00, 0x00, 0x00]); + assert.deepEqual(rgba.toChannels(0x10101010), [0x10, 0x10, 0x10, 0x10]); + assert.deepEqual(rgba.toChannels(0x20202020), [0x20, 0x20, 0x20, 0x20]); + assert.deepEqual(rgba.toChannels(0x30303030), [0x30, 0x30, 0x30, 0x30]); + assert.deepEqual(rgba.toChannels(0x40404040), [0x40, 0x40, 0x40, 0x40]); + assert.deepEqual(rgba.toChannels(0x50505050), [0x50, 0x50, 0x50, 0x50]); + assert.deepEqual(rgba.toChannels(0x60606060), [0x60, 0x60, 0x60, 0x60]); + assert.deepEqual(rgba.toChannels(0x70707070), [0x70, 0x70, 0x70, 0x70]); + assert.deepEqual(rgba.toChannels(0x80808080), [0x80, 0x80, 0x80, 0x80]); + assert.deepEqual(rgba.toChannels(0x90909090), [0x90, 0x90, 0x90, 0x90]); + assert.deepEqual(rgba.toChannels(0xa0a0a0a0), [0xa0, 0xa0, 0xa0, 0xa0]); + assert.deepEqual(rgba.toChannels(0xb0b0b0b0), [0xb0, 0xb0, 0xb0, 0xb0]); + assert.deepEqual(rgba.toChannels(0xc0c0c0c0), [0xc0, 0xc0, 0xc0, 0xc0]); + assert.deepEqual(rgba.toChannels(0xd0d0d0d0), [0xd0, 0xd0, 0xd0, 0xd0]); + assert.deepEqual(rgba.toChannels(0xe0e0e0e0), [0xe0, 0xe0, 0xe0, 0xe0]); + assert.deepEqual(rgba.toChannels(0xf0f0f0f0), [0xf0, 0xf0, 0xf0, 0xf0]); + assert.deepEqual(rgba.toChannels(0xffffffff), [0xff, 0xff, 0xff, 0xff]); + }); + }); + }); + + describe('toPaddedHex', () => { + it('should convert numbers to 2-digit hex values', () => { + assert.equal(toPaddedHex(0x00), '00'); + assert.equal(toPaddedHex(0x10), '10'); + assert.equal(toPaddedHex(0x20), '20'); + assert.equal(toPaddedHex(0x30), '30'); + assert.equal(toPaddedHex(0x40), '40'); + assert.equal(toPaddedHex(0x50), '50'); + assert.equal(toPaddedHex(0x60), '60'); + assert.equal(toPaddedHex(0x70), '70'); + assert.equal(toPaddedHex(0x80), '80'); + assert.equal(toPaddedHex(0x90), '90'); + assert.equal(toPaddedHex(0xa0), 'a0'); + assert.equal(toPaddedHex(0xb0), 'b0'); + assert.equal(toPaddedHex(0xc0), 'c0'); + assert.equal(toPaddedHex(0xd0), 'd0'); + assert.equal(toPaddedHex(0xe0), 'e0'); + assert.equal(toPaddedHex(0xf0), 'f0'); + assert.equal(toPaddedHex(0xff), 'ff'); + }); + }); + + describe('contrastRatio', () => { + it('should calculate the relative luminance of the color', () => { + assert.equal(contrastRatio(0, 0), 1); + assert.equal(contrastRatio(0, 0.5), 11); + assert.equal(contrastRatio(0, 1), 21); + }); + it('should work regardless of the parameter order', () => { + assert.equal(contrastRatio(0, 1), 21); + assert.equal(contrastRatio(1, 0), 21); + }); + }); +}); diff --git a/src/browser/Color.ts b/src/browser/Color.ts new file mode 100644 index 000000000..9649227e2 --- /dev/null +++ b/src/browser/Color.ts @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IColor } from 'browser/Types'; + +/** + * Helper functions where the source type is "channels" (individual color channels as numbers). + */ +export namespace channels { + export function toCss(r: number, g: number, b: number, a?: number): string { + if (a !== undefined) { + return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`; + } + return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`; + } + + export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number { + // >>> 0 forces an unsigned int + return (r << 24 | g << 16 | b << 8 | a) >>> 0; + } +} + +/** + * Helper functions where the source type is `IColor`. + */ +export namespace color { + export function blend(bg: IColor, fg: IColor): IColor { + const a = (fg.rgba & 0xFF) / 255; + if (a === 1) { + return { + css: fg.css, + rgba: fg.rgba + }; + } + const fgR = (fg.rgba >> 24) & 0xFF; + const fgG = (fg.rgba >> 16) & 0xFF; + const fgB = (fg.rgba >> 8) & 0xFF; + const bgR = (bg.rgba >> 24) & 0xFF; + const bgG = (bg.rgba >> 16) & 0xFF; + const bgB = (bg.rgba >> 8) & 0xFF; + const r = bgR + Math.round((fgR - bgR) * a); + const g = bgG + Math.round((fgG - bgG) * a); + const b = bgB + Math.round((fgB - bgB) * a); + const css = channels.toCss(r, g, b); + const rgba = channels.toRgba(r, g, b); + return { css, rgba }; + } + + export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined { + const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio); + if (!result) { + return undefined; + } + return rgba.toColor( + (result >> 24 & 0xFF), + (result >> 16 & 0xFF), + (result >> 8 & 0xFF) + ); + } + + export function opaque(color: IColor): IColor { + const rgbaColor = (color.rgba | 0xFF) >>> 0; + const [r, g, b] = rgba.toChannels(rgbaColor); + return { + css: channels.toCss(r, g, b), + rgba: rgbaColor + }; + } +} + +/** + * Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa'). + */ +export namespace css { + export function toColor(css: string): IColor { + return { + css, + rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0 + }; + } +} + +/** + * Helper functions where the source type is "rgb" (number: 0xrrggbb). + */ +export namespace rgb { + /** + * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio + * between two colors. + * @param rgb The color to use. + * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef + */ + export function relativeLuminance(rgb: number): number { + return relativeLuminance2( + (rgb >> 16) & 0xFF, + (rgb >> 8 ) & 0xFF, + (rgb ) & 0xFF); + } + + /** + * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio + * between two colors. + * @param r The red channel (0x00 to 0xFF). + * @param g The green channel (0x00 to 0xFF). + * @param b The blue channel (0x00 to 0xFF). + * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef + */ + export function relativeLuminance2(r: number, g: number, b: number): number { + const rs = r / 255; + const gs = g / 255; + const bs = b / 255; + const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4); + const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4); + const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4); + return rr * 0.2126 + rg * 0.7152 + rb * 0.0722; + } +} + +/** + * Helper functions where the source type is "rgba" (number: 0xrrggbbaa). + */ +export namespace rgba { + export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined { + const bgL = rgb.relativeLuminance(bgRgba >> 8); + const fgL = rgb.relativeLuminance(fgRgba >> 8); + const cr = contrastRatio(bgL, fgL); + if (cr < ratio) { + if (fgL < bgL) { + return reduceLuminance(bgRgba, fgRgba, ratio); + } + return increaseLuminance(bgRgba, fgRgba, ratio); + } + return undefined; + } + + export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number { + // This is a naive but fast approach to reducing luminance as converting to + // HSL and back is expensive + const bgR = (bgRgba >> 24) & 0xFF; + const bgG = (bgRgba >> 16) & 0xFF; + const bgB = (bgRgba >> 8) & 0xFF; + let fgR = (fgRgba >> 24) & 0xFF; + let fgG = (fgRgba >> 16) & 0xFF; + let fgB = (fgRgba >> 8) & 0xFF; + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) { + // Reduce by 10% until the ratio is hit + fgR -= Math.max(0, Math.ceil(fgR * 0.1)); + fgG -= Math.max(0, Math.ceil(fgG * 0.1)); + fgB -= Math.max(0, Math.ceil(fgB * 0.1)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + } + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; + } + + export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number { + // This is a naive but fast approach to increasing luminance as converting to + // HSL and back is expensive + const bgR = (bgRgba >> 24) & 0xFF; + const bgG = (bgRgba >> 16) & 0xFF; + const bgB = (bgRgba >> 8) & 0xFF; + let fgR = (fgRgba >> 24) & 0xFF; + let fgG = (fgRgba >> 16) & 0xFF; + let fgB = (fgRgba >> 8) & 0xFF; + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) { + // Increase by 10% until the ratio is hit + fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1)); + fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1)); + fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + } + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; + } + + export function toChannels(value: number): [number, number, number, number] { + return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]; + } + + export function toColor(r: number, g: number, b: number): IColor { + return { + css: channels.toCss(r, g, b), + rgba: channels.toRgba(r, g, b) + }; + } +} + +export function toPaddedHex(c: number): string { + const s = c.toString(16); + return s.length < 2 ? '0' + s : s; +} + +/** + * Gets the contrast ratio between two relative luminance values. + * @param l1 The first relative luminance. + * @param l2 The first relative luminance. + * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef + */ +export function contrastRatio(l1: number, l2: number): number { + if (l1 < l2) { + return (l2 + 0.05) / (l1 + 0.05); + } + return (l1 + 0.05) / (l2 + 0.05); +} diff --git a/src/browser/ColorContrastCache.test.ts b/src/browser/ColorContrastCache.test.ts new file mode 100644 index 000000000..17e5df383 --- /dev/null +++ b/src/browser/ColorContrastCache.test.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { ColorContrastCache } from 'browser/ColorContrastCache'; + +describe('ColorContrastCache', () => { + let cache: ColorContrastCache; + + beforeEach(() => { + cache = new ColorContrastCache(); + }); + + it('should save and get color values', () => { + assert.strictEqual(cache.getColor(0x01, 0x00), undefined); + cache.setColor(0x01, 0x01, null); + assert.strictEqual(cache.getColor(0x01, 0x01), null); + cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff}); + assert.deepEqual(cache.getColor(0x01, 0x02), { css: '#030303', rgba: 0x030303ff}); + }); + + it('should save and get css values', () => { + assert.strictEqual(cache.getCss(0x01, 0x00), undefined); + cache.setCss(0x01, 0x01, null); + assert.strictEqual(cache.getCss(0x01, 0x01), null); + cache.setCss(0x01, 0x02, '#030303'); + assert.deepEqual(cache.getCss(0x01, 0x02), '#030303'); + }); + + it('should clear all values on clear', () => { + cache.setColor(0x01, 0x01, null); + cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff}); + cache.setCss(0x01, 0x01, null); + cache.setCss(0x01, 0x02, '#030303'); + cache.clear(); + assert.strictEqual(cache.getColor(0x01, 0x01), undefined); + assert.strictEqual(cache.getColor(0x01, 0x02), undefined); + assert.strictEqual(cache.getCss(0x01, 0x01), undefined); + assert.strictEqual(cache.getCss(0x01, 0x02), undefined); + }); +}); diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts new file mode 100644 index 000000000..b96b66cc4 --- /dev/null +++ b/src/browser/ColorContrastCache.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IColor, IColorContrastCache } from 'browser/Types'; + +export class ColorContrastCache implements IColorContrastCache { + private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; + private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {}; + + public clear(): void { + this._color = {}; + this._rgba = {}; + } + + public setCss(bg: number, fg: number, value: string | null): void { + if (!this._rgba[bg]) { + this._rgba[bg] = {}; + } + this._rgba[bg]![fg] = value; + } + + public getCss(bg: number, fg: number): string | null | undefined { + return this._rgba[bg] ? this._rgba[bg]![fg] : undefined; + } + + public setColor(bg: number, fg: number, value: IColor | null): void { + if (!this._color[bg]) { + this._color[bg] = {}; + } + this._color[bg]![fg] = value; + } + + public getColor(bg: number, fg: number): IColor | null | undefined { + return this._color[bg] ? this._color[bg]![fg] : undefined; + } +} diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts index a213c6166..39a8f8c2f 100644 --- a/src/browser/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -34,7 +34,7 @@ describe('ColorManager', () => { describe('constructor', () => { it('should fill all colors with values', () => { for (const key of Object.keys(cm.colors)) { - if (key !== 'ansi') { + if (key !== 'ansi' && key !== 'contrastCache') { // A #rrggbb or rgba(...) assert.ok((cm.colors)[key].css.length >= 7); } diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 70d21a7a4..c8224a921 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -3,16 +3,18 @@ * @license MIT */ -import { IColorManager, IColor, IColorSet } from 'browser/Types'; +import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; +import { channels, color, css } from 'browser/Color'; +import { ColorContrastCache } from 'browser/ColorContrastCache'; -const DEFAULT_FOREGROUND = fromHex('#ffffff'); -const DEFAULT_BACKGROUND = fromHex('#000000'); -const DEFAULT_CURSOR = fromHex('#ffffff'); -const DEFAULT_CURSOR_ACCENT = fromHex('#000000'); +const DEFAULT_FOREGROUND = css.toColor('#ffffff'); +const DEFAULT_BACKGROUND = css.toColor('#000000'); +const DEFAULT_CURSOR = css.toColor('#ffffff'); +const DEFAULT_CURSOR_ACCENT = css.toColor('#000000'); const DEFAULT_SELECTION = { css: 'rgba(255, 255, 255, 0.3)', - rgba: 0xFFFFFF77 + rgba: 0xFFFFFF4D }; // An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy @@ -20,23 +22,23 @@ const DEFAULT_SELECTION = { export const DEFAULT_ANSI_COLORS = (() => { const colors = [ // dark: - fromHex('#2e3436'), - fromHex('#cc0000'), - fromHex('#4e9a06'), - fromHex('#c4a000'), - fromHex('#3465a4'), - fromHex('#75507b'), - fromHex('#06989a'), - fromHex('#d3d7cf'), + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), // bright: - fromHex('#555753'), - fromHex('#ef2929'), - fromHex('#8ae234'), - fromHex('#fce94f'), - fromHex('#729fcf'), - fromHex('#ad7fa8'), - fromHex('#34e2e2'), - fromHex('#eeeeec') + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') ]; // Fill in the remaining 240 ANSI colors. @@ -47,37 +49,23 @@ export const DEFAULT_ANSI_COLORS = (() => { const g = v[(i / 6) % 6 | 0]; const b = v[i % 6]; colors.push({ - css: `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`, - // Use >>> 0 to force a conversion to an unsigned int - rgba: ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0 + css: channels.toCss(r, g, b), + rgba: channels.toRgba(r, g, b) }); } // Generate greys (232-255) for (let i = 0; i < 24; i++) { const c = 8 + i * 10; - const ch = toPaddedHex(c); colors.push({ - css: `#${ch}${ch}${ch}`, - rgba: ((c << 24) | (c << 16) | (c << 8) | 0xFF) >>> 0 + css: channels.toCss(c, c, c), + rgba: channels.toRgba(c, c, c) }); } return colors; })(); -function fromHex(css: string): IColor { - return { - css, - rgba: parseInt(css.slice(1), 16) << 8 | 0xFF - }; -} - -function toPaddedHex(c: number): string { - const s = c.toString(16); - return s.length < 2 ? '0' + s : s; -} - /** * Manages the source of truth for a terminal's colors. */ @@ -85,6 +73,7 @@ export class ColorManager implements IColorManager { public colors: IColorSet; private _ctx: CanvasRenderingContext2D; private _litmusColor: CanvasGradient; + private _contrastCache: IColorContrastCache; constructor(document: Document, public allowTransparency: boolean) { const canvas = document.createElement('canvas'); @@ -97,16 +86,25 @@ export class ColorManager implements IColorManager { this._ctx = ctx; this._ctx.globalCompositeOperation = 'copy'; this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1); + this._contrastCache = new ColorContrastCache(); this.colors = { foreground: DEFAULT_FOREGROUND, background: DEFAULT_BACKGROUND, cursor: DEFAULT_CURSOR, cursorAccent: DEFAULT_CURSOR_ACCENT, selection: DEFAULT_SELECTION, - ansi: DEFAULT_ANSI_COLORS.slice() + selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), + ansi: DEFAULT_ANSI_COLORS.slice(), + contrastCache: this._contrastCache }; } + public onOptionsChange(key: string): void { + if (key === 'minimumContrastRatio') { + this._contrastCache.clear(); + } + } + /** * Sets the terminal's theme. * @param theme The theme to use. If a partial theme is provided then default @@ -118,6 +116,7 @@ export class ColorManager implements IColorManager { this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true); this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true); this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true); + this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selection); this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]); this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]); this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]); @@ -134,6 +133,8 @@ export class ColorManager implements IColorManager { this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); + // Clear our the cache + this._contrastCache.clear(); } private _parseColor( @@ -158,33 +159,48 @@ export class ColorManager implements IColorManager { this._ctx.fillRect(0, 0, 1, 1); const data = this._ctx.getImageData(0, 0, 1, 1).data; - if (!allowTransparency && data[3] !== 0xFF) { - // Ideally we'd just ignore the alpha channel, but... - // - // Browsers may not give back exactly the same RGB values we put in, because most/all - // convert the color to a pre-multiplied representation. getImageData converts that back to - // a un-premultipled representation, but the precision loss may make the RGB channels unuable - // on their own. - // - // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns - // into 0x00000000. - // - // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, - // pixels that have just been set using putImageData() might be returned to an equivalent - // getImageData() as different values." - // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation - // - // So let's just use the fallback color in this case instead. - console.warn( - `Color: ${css} is using transparency, but allowTransparency is false. ` + - `Using fallback ${fallback.css}.` - ); - return fallback; + // Check if the printed color was transparent + if (data[3] !== 0xFF) { + if (!allowTransparency) { + // Ideally we'd just ignore the alpha channel, but... + // + // Browsers may not give back exactly the same RGB values we put in, because most/all + // convert the color to a pre-multiplied representation. getImageData converts that back to + // a un-premultipled representation, but the precision loss may make the RGB channels unuable + // on their own. + // + // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns + // into 0x00000000. + // + // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, + // pixels that have just been set using putImageData() might be returned to an equivalent + // getImageData() as different values." + // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation + // + // So let's just use the fallback color in this case instead. + console.warn( + `Color: ${css} is using transparency, but allowTransparency is false. ` + + `Using fallback ${fallback.css}.` + ); + return fallback; + } + + // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color + // the color value has alpha less than 1.0, and the string is the color value in the CSS rgba() + const [r, g, b, a] = this._ctx.fillStyle.substring(5, this._ctx.fillStyle.length - 1).split(',').map(component => Number(component)); + const alpha = Math.round(a * 255); + const rgba: number = channels.toRgba(r, g, b, alpha); + return { + rgba, + css: channels.toCss(r, g, b, alpha) + }; } return { - css, - rgba: (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0 + // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color + // if it has alpha equal to 1.0, then the string is a lowercase six-digit hex value, prefixed with a "#" character + css: this._ctx.fillStyle, + rgba: channels.toRgba(data[0], data[1], data[2], data[3]) }; } } diff --git a/src/browser/Lifecycle.ts b/src/browser/Lifecycle.ts index 1344ea9bc..a8172db50 100644 --- a/src/browser/Lifecycle.ts +++ b/src/browser/Lifecycle.ts @@ -17,12 +17,13 @@ export function addDisposableDomListener( useCapture?: boolean ): IDisposable { node.addEventListener(type, handler, useCapture); + let disposed = false; return { dispose: () => { - if (!handler) { - // Already disposed + if (!disposed) { return; } + disposed = true; node.removeEventListener(type, handler, useCapture); } }; diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts index 53a32aa7f..58aca70eb 100644 --- a/src/browser/Linkifier.ts +++ b/src/browser/Linkifier.ts @@ -306,7 +306,9 @@ export class Linkifier implements ILinkifier { e => { this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); if (matcher.hoverTooltipCallback) { - matcher.hoverTooltipCallback(e, uri); + // Note that IViewportRange use 1-based coordinates to align with escape sequences such + // as CUP which use 1,1 as the default for row/col + matcher.hoverTooltipCallback(e, uri, { start: { x: x1, y: y1 }, end: { x: x2, y: y2 } }); } }, () => { diff --git a/src/browser/MouseZoneManager.ts b/src/browser/MouseZoneManager.ts index 7eb7c5f89..d697c1334 100644 --- a/src/browser/MouseZoneManager.ts +++ b/src/browser/MouseZoneManager.ts @@ -175,11 +175,9 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { // Find the active zone, prevent event propagation if found to prevent other // components from handling the mouse event. const zone = this._findZoneEventAt(e); - if (zone) { - if (zone.willLinkActivate(e)) { - e.preventDefault(); - e.stopImmediatePropagation(); - } + if (zone?.willLinkActivate(e)) { + e.preventDefault(); + e.stopImmediatePropagation(); } } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 894beb36c..31d00af85 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -8,6 +8,7 @@ import { IDisposable } from 'common/Types'; export interface IColorManager { colors: IColorSet; + onOptionsChange(key: string): void; } export interface IColor { @@ -21,7 +22,18 @@ export interface IColorSet { cursor: IColor; cursorAccent: IColor; selection: IColor; + /** The selection blended on top of background. */ + selectionOpaque: IColor; ansi: IColor[]; + contrastCache: IColorContrastCache; +} + +export interface IColorContrastCache { + clear(): void; + setCss(bg: number, fg: number, value: string | null): void; + getCss(bg: number, fg: number): string | null | undefined; + setColor(bg: number, fg: number, value: IColor | null): void; + getColor(bg: number, fg: number): IColor | null | undefined; } export interface IPartialColorSet { @@ -35,7 +47,7 @@ export interface IPartialColorSet { export interface IViewport extends IDisposable { scrollBarWidth: number; - syncScrollArea(): void; + syncScrollArea(immediate?: boolean): void; getLinesScrolled(ev: WheelEvent): number; onWheel(ev: WheelEvent): boolean; onTouchStart(ev: TouchEvent): void; @@ -43,14 +55,25 @@ export interface IViewport extends IDisposable { onThemeChange(colors: IColorSet): void; } +export interface IViewportRange { + start: IViewportRangePosition; + end: IViewportRangePosition; +} + +export interface IViewportRangePosition { + x: number; + y: number; +} + export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; +export type LinkMatcherHoverTooltipCallback = (event: MouseEvent, uri: string, position: IViewportRange) => void; export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; export interface ILinkMatcher { id: number; regex: RegExp; handler: LinkMatcherHandler; - hoverTooltipCallback?: LinkMatcherHandler; + hoverTooltipCallback?: LinkMatcherHoverTooltipCallback; hoverLeaveCallback?: () => void; matchIndex?: number; validationCallback?: LinkMatcherValidationCallback; @@ -96,7 +119,7 @@ export interface ILinkMatcherOptions { /** * A callback that fires when the mouse hovers over a link. */ - tooltipCallback?: LinkMatcherHandler; + tooltipCallback?: LinkMatcherHoverTooltipCallback; /** * A callback that fires when the mouse leaves a link that was hovered. */ diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 4f9363d99..b88f381d8 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IViewport } from 'browser/Types'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; -import { IBufferService } from 'common/services/Services'; +import { IBufferService, IOptionsService } from 'common/services/Services'; const FALLBACK_SCROLL_BAR_WIDTH = 15; @@ -37,6 +37,7 @@ export class Viewport extends Disposable implements IViewport { private readonly _viewportElement: HTMLElement, private readonly _scrollArea: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IRenderService private readonly _renderService: IRenderService ) { @@ -60,7 +61,14 @@ export class Viewport extends Disposable implements IViewport { * Refreshes row height, setting line-height, viewport height and scroll area height if * necessary. */ - private _refresh(): void { + private _refresh(immediate: boolean): void { + if (immediate) { + this._innerRefresh(); + if (this._refreshAnimationFrame !== null) { + cancelAnimationFrame(this._refreshAnimationFrame); + } + return; + } if (this._refreshAnimationFrame === null) { this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh()); } @@ -88,40 +96,39 @@ export class Viewport extends Disposable implements IViewport { this._refreshAnimationFrame = null; } - /** * Updates dimensions and synchronizes the scroll area if necessary. */ - public syncScrollArea(): void { + public syncScrollArea(immediate: boolean = false): void { // If buffer height changed if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) { this._lastRecordedBufferLength = this._bufferService.buffer.lines.length; - this._refresh(); + this._refresh(immediate); return; } // If viewport height changed if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) { - this._refresh(); + this._refresh(immediate); return; } // If the buffer position doesn't match last scroll top const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight; if (this._lastScrollTop !== newScrollTop) { - this._refresh(); + this._refresh(immediate); return; } // If element's scroll top changed, this can happen when hiding the element if (this._lastScrollTop !== this._viewportElement.scrollTop) { - this._refresh(); + this._refresh(immediate); return; } // If row height changed if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) { - this._refresh(); + this._refresh(immediate); return; } } @@ -191,7 +198,7 @@ export class Viewport extends Disposable implements IViewport { } // Fallback to WheelEvent.DOM_DELTA_PIXEL - let amount = ev.deltaY; + let amount = this._applyScrollModifier(ev.deltaY, ev); if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { amount *= this._currentRowHeight; } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { @@ -212,7 +219,7 @@ export class Viewport extends Disposable implements IViewport { } // Fallback to WheelEvent.DOM_DELTA_LINE - let amount = ev.deltaY; + let amount = this._applyScrollModifier(ev.deltaY, ev); if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { amount /= this._currentRowHeight + 0.0; // Prevent integer division this._wheelPartialScroll += amount; @@ -224,6 +231,18 @@ export class Viewport extends Disposable implements IViewport { return amount; } + private _applyScrollModifier(amount: number, ev: WheelEvent): number { + const modifier = this._optionsService.options.fastScrollModifier; + // Multiply the scroll speed when the modifier is down + if ((modifier === 'alt' && ev.altKey) || + (modifier === 'ctrl' && ev.ctrlKey) || + (modifier === 'shift' && ev.shiftKey)) { + return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity; + } + + return amount * this._optionsService.options.scrollSensitivity; + } + /** * Handles the touchstart event, recording the touch occurred. * @param ev The touch event. diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index e9ad5b3a2..f109f42e6 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -5,16 +5,17 @@ import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; import { ICellData } from 'common/Types'; -import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; import { AttributeData } from 'common/buffer/AttributeData'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IColor } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; +import { channels, color, rgba } from 'browser/Color'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -60,9 +61,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { public dispose(): void { this._container.removeChild(this._canvas); - if (this._charAtlas) { - this._charAtlas.dispose(); - } + this._charAtlas?.dispose(); } private _initCanvas(): void { @@ -172,11 +171,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to fill. * @param y The row to fill. */ - protected _fillLeftLineAtCell(x: number, y: number): void { + protected _fillLeftLineAtCell(x: number, y: number, width: number): void { this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, - window.devicePixelRatio, + window.devicePixelRatio * width, this._scaledCellHeight); } @@ -264,13 +263,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param bold Whether the text is bold. */ protected _drawChars(cell: ICellData, x: number, y: number): void { + const contrastColor = this._getContrastColor(cell); // skip cache right away if we draw in RGB // Note: to avoid bad runtime JoinedCellData will be skipped // in the cache handler itself (atlasDidDraw == false) and // fall through to uncached later down below - if (cell.isFgRGB() || cell.isBgRGB()) { - this._drawUncachedChars(cell, x, y); + if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) { + this._drawUncachedChars(cell, x, y, contrastColor); return; } @@ -284,7 +284,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); } - const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; + const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8; fg += drawInBrightColor ? 8 : 0; this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR; @@ -316,21 +316,29 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to draw at. * @param y The row to draw at. */ - private _drawUncachedChars(cell: ICellData, x: number, y: number): void { + 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'; if (cell.isInverse()) { - if (cell.isBgDefault()) { - this._ctx.fillStyle = this._colors.background.css; + if (fgOverride) { + this._ctx.fillStyle = fgOverride.css; + } else if (cell.isBgDefault()) { + this._ctx.fillStyle = color.opaque(this._colors.background).css; } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { - this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + let bg = cell.getBgColor(); + if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { + bg += 8; + } + this._ctx.fillStyle = this._colors.ansi[bg].css; } } else { - if (cell.isFgDefault()) { + if (fgOverride) { + this._ctx.fillStyle = fgOverride.css; + } else if (cell.isFgDefault()) { this._ctx.fillStyle = this._colors.foreground.css; } else if (cell.isFgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; @@ -381,5 +389,88 @@ export abstract class BaseRenderLayer implements IRenderLayer { return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`; } + + private _getContrastColor(cell: CellData): IColor | undefined { + if (this._optionsService.options.minimumContrastRatio === 1) { + return undefined; + } + + // Try get from cache first + const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); + if (adjustedColor !== undefined) { + return adjustedColor || undefined; + } + + let fgColor = cell.getFgColor(); + let fgColorMode = cell.getFgColorMode(); + let bgColor = cell.getBgColor(); + let bgColorMode = cell.getBgColorMode(); + const isInverse = !!cell.isInverse(); + const isBold = !!cell.isInverse(); + if (isInverse) { + const temp = fgColor; + fgColor = bgColor; + bgColor = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; + } + + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse); + const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); + const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio); + + if (!result) { + this._colors.contrastCache.setColor(cell.bg, cell.fg, null); + return undefined; + } + + const color: IColor = { + css: channels.toCss( + (result >> 24) & 0xFF, + (result >> 16) & 0xFF, + (result >> 8) & 0xFF + ), + rgba: result + }; + this._colors.contrastCache.setColor(cell.bg, cell.fg, color); + + return color; + } + + private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number { + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + return this._colors.ansi[bgColor].rgba; + case Attributes.CM_RGB: + return bgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._colors.foreground.rgba; + } + return this._colors.background.rgba; + } + } + + private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } + return this._colors.ansi[fgColor].rgba; + case Attributes.CM_RGB: + return fgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._colors.background.rgba; + } + return this._colors.foreground.rgba; + } + } } diff --git a/src/browser/renderer/CharacterJoinerRegistry.ts b/src/browser/renderer/CharacterJoinerRegistry.ts index a5abe80f5..5385b76b4 100644 --- a/src/browser/renderer/CharacterJoinerRegistry.ts +++ b/src/browser/renderer/CharacterJoinerRegistry.ts @@ -303,7 +303,6 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { // current range ranges[i - 1][1] = Math.max(newRange[1], range[1]); ranges.splice(i, 1); - inRange = false; return ranges; } diff --git a/src/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts similarity index 74% rename from src/renderer/CursorRenderLayer.ts rename to src/browser/renderer/CursorRenderLayer.ts index f847c5f69..64ecf33fa 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; -import { BaseRenderLayer } from '../browser/renderer/BaseRenderLayer'; -import { ITerminal } from '../Types'; +import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; +import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; +import { IEventEmitter } from 'common/EventEmitter'; +import { ICoreBrowserService } from 'browser/services/Services'; interface ICursorState { x: number; @@ -27,25 +28,27 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void}; - private _cursorBlinkStateManager: CursorBlinkStateManager; + private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; private _cell: ICellData = new CellData(); constructor( container: HTMLElement, zIndex: number, colors: IColorSet, - private _terminal: ITerminal, rendererId: number, + private _onRequestRefreshRowsEvent: IEventEmitter, readonly bufferService: IBufferService, - readonly optionsService: IOptionsService + readonly optionsService: IOptionsService, + private readonly _coreService: ICoreService, + private readonly _coreBrowserService: ICoreBrowserService ) { super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService); this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; this._cursorRenderers = { 'bar': this._renderBarCursor.bind(this), @@ -59,11 +62,11 @@ export class CursorRenderLayer extends BaseRenderLayer { super.resize(dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; } @@ -71,7 +74,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.dispose(); - this._cursorBlinkStateManager = null; + this._cursorBlinkStateManager = undefined; this.onOptionsChanged(); } } @@ -80,33 +83,31 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.pause(); } - this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); + this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } public onFocus(): void { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.resume(); } else { - this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); + this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } } public onOptionsChanged(): void { if (this._optionsService.options.cursorBlink) { if (!this._cursorBlinkStateManager) { - this._cursorBlinkStateManager = new CursorBlinkStateManager(this._terminal.isFocused, () => { + this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => { this._render(true); }); } } else { - if (this._cursorBlinkStateManager) { - this._cursorBlinkStateManager.dispose(); - this._cursorBlinkStateManager = null; - } - // Request a refresh from the terminal as management of rendering is being - // moved back to the terminal - this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); + this._cursorBlinkStateManager?.dispose(); + this._cursorBlinkStateManager = undefined; } + // Request a refresh from the terminal as management of rendering is being + // moved back to the terminal + this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } public onCursorMove(): void { @@ -125,7 +126,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _render(triggeredByAnimationFrame: boolean): void { // Don't draw the cursor if it's hidden - if (!this._terminal.cursorState || this._terminal.cursorHidden) { + if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) { this._clearCursor(); return; } @@ -139,21 +140,26 @@ export class CursorRenderLayer extends BaseRenderLayer { return; } - this._bufferService.buffer.lines.get(cursorY).loadCell(this._bufferService.buffer.x, this._cell); + this._bufferService.buffer.lines.get(cursorY)!.loadCell(this._bufferService.buffer.x, this._cell); if (this._cell.content === undefined) { return; } - if (!this._terminal.isFocused) { + if (!this._coreBrowserService.isFocused) { this._clearCursor(); this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell); + const cursorStyle = this._optionsService.options.cursorStyle; + if (cursorStyle && cursorStyle !== 'block') { + this._cursorRenderers[cursorStyle](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell); + } else { + this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell); + } this._ctx.restore(); this._state.x = this._bufferService.buffer.x; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; - this._state.style = this._optionsService.options.cursorStyle; + this._state.style = cursorStyle; this._state.width = this._cell.getWidth(); return; } @@ -168,7 +174,7 @@ export class CursorRenderLayer extends BaseRenderLayer { // The cursor is already in the correct spot, don't redraw if (this._state.x === this._bufferService.buffer.x && this._state.y === viewportRelativeCursorY && - this._state.isFocused === this._terminal.isFocused && + this._state.isFocused === this._coreBrowserService.isFocused && this._state.style === this._optionsService.options.cursorStyle && this._state.width === this._cell.getWidth()) { return; @@ -191,11 +197,11 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._state) { this._clearCells(this._state.x, this._state.y, this._state.width, 1); this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; } } @@ -203,7 +209,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._fillLeftLineAtCell(x, y); + this._fillLeftLineAtCell(x, y, this._optionsService.options.cursorWidth); this._ctx.restore(); } @@ -234,16 +240,16 @@ export class CursorRenderLayer extends BaseRenderLayer { class CursorBlinkStateManager { public isCursorVisible: boolean; - private _animationFrame: number; - private _blinkStartTimeout: number; - private _blinkInterval: number; + private _animationFrame: number | undefined; + private _blinkStartTimeout: number | undefined; + private _blinkInterval: number | undefined; /** * The time at which the animation frame was restarted, this is used on the * next render to restart the timers so they don't need to restart the timers * multiple times over a short period. */ - private _animationTimeRestarted: number; + private _animationTimeRestarted: number | undefined; constructor( isFocused: boolean, @@ -260,15 +266,15 @@ class CursorBlinkStateManager { public dispose(): void { if (this._blinkInterval) { window.clearInterval(this._blinkInterval); - this._blinkInterval = null; + this._blinkInterval = undefined; } if (this._blinkStartTimeout) { window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = null; + this._blinkStartTimeout = undefined; } if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = null; + this._animationFrame = undefined; } } @@ -283,7 +289,7 @@ class CursorBlinkStateManager { if (!this._animationFrame) { this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); } } @@ -303,7 +309,7 @@ class CursorBlinkStateManager { // started if (this._animationTimeRestarted) { const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; if (time > 0) { this._restartInterval(time); return; @@ -314,7 +320,7 @@ class CursorBlinkStateManager { this.isCursorVisible = false; this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); // Setup the blink interval @@ -324,7 +330,7 @@ class CursorBlinkStateManager { // calc time diff // Make restart interval do a setTimeout initially? const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; this._restartInterval(time); return; } @@ -333,7 +339,7 @@ class CursorBlinkStateManager { this.isCursorVisible = !this.isCursorVisible; this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); }, BLINK_INTERVAL); }, timeToStart); @@ -343,20 +349,20 @@ class CursorBlinkStateManager { this.isCursorVisible = true; if (this._blinkInterval) { window.clearInterval(this._blinkInterval); - this._blinkInterval = null; + this._blinkInterval = undefined; } if (this._blinkStartTimeout) { window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = null; + this._blinkStartTimeout = undefined; } if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = null; + this._animationFrame = undefined; } } public resume(): void { - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; this._restartInterval(); this.restartBlinkAnimation(); } diff --git a/src/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts similarity index 64% rename from src/renderer/Renderer.ts rename to src/browser/renderer/Renderer.ts index d3977af37..26b464cbd 100644 --- a/src/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -3,18 +3,18 @@ * @license MIT */ -import { TextRenderLayer } from '../browser/renderer/TextRenderLayer'; -import { SelectionRenderLayer } from '../browser/renderer/SelectionRenderLayer'; -import { CursorRenderLayer } from './CursorRenderLayer'; -import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry } from 'browser/renderer/Types'; -import { ITerminal } from '../Types'; -import { LinkRenderLayer } from '../browser/renderer/LinkRenderLayer'; +import { TextRenderLayer } from 'browser/renderer/TextRenderLayer'; +import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer'; +import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer'; +import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; +import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer'; import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry'; import { Disposable } from 'common/Lifecycle'; -import { IColorSet } from 'browser/Types'; -import { ICharSizeService } from 'browser/services/Services'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IColorSet, ILinkifier } from 'browser/Types'; +import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; +import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; let nextRendererId = 1; @@ -27,36 +27,42 @@ export class Renderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; + private _onRequestRefreshRows = new EventEmitter(); + public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } + constructor( private _colors: IColorSet, - private readonly _terminal: ITerminal, - readonly bufferService: IBufferService, - private readonly _charSizeService: ICharSizeService, - readonly optionsService: IOptionsService + private readonly _screenElement: HTMLElement, + private readonly _linkifier: ILinkifier, + @IBufferService private readonly _bufferService: IBufferService, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IOptionsService private readonly _optionsService: IOptionsService, + @ICoreService readonly coreService: ICoreService, + @ICoreBrowserService readonly coreBrowserService: ICoreBrowserService ) { super(); - const allowTransparency = this._terminal.options.allowTransparency; - this._characterJoinerRegistry = new CharacterJoinerRegistry(bufferService); + const allowTransparency = this._optionsService.options.allowTransparency; + this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService); this._renderLayers = [ - new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, bufferService, optionsService), - new SelectionRenderLayer(this._terminal.screenElement, 1, this._colors, this._id, bufferService, optionsService), - new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._id, this._terminal.linkifier, bufferService, optionsService), - new CursorRenderLayer(this._terminal.screenElement, 3, this._colors, this._terminal, this._id, bufferService, optionsService) + 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, this._linkifier, this._bufferService, _optionsService), + new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRefreshRows, this._bufferService, _optionsService, coreService, coreBrowserService) ]; this.dimensions = { - scaledCharWidth: null, - scaledCharHeight: null, - scaledCellWidth: null, - scaledCellHeight: null, - scaledCharLeft: null, - scaledCharTop: null, - scaledCanvasWidth: null, - scaledCanvasHeight: null, - canvasWidth: null, - canvasHeight: null, - actualCellWidth: null, - actualCellHeight: null + scaledCharWidth: 0, + scaledCharHeight: 0, + scaledCellWidth: 0, + scaledCellHeight: 0, + scaledCharLeft: 0, + scaledCharTop: 0, + scaledCanvasWidth: 0, + scaledCanvasHeight: 0, + canvasWidth: 0, + canvasHeight: 0, + actualCellWidth: 0, + actualCellHeight: 0 }; this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); @@ -74,7 +80,7 @@ export class Renderer extends Disposable implements IRenderer { // and the terminal needs to refreshed if (this._devicePixelRatio !== window.devicePixelRatio) { this._devicePixelRatio = window.devicePixelRatio; - this.onResize(this._terminal.cols, this._terminal.rows); + this.onResize(this._bufferService.cols, this._bufferService.rows); } } @@ -96,12 +102,12 @@ export class Renderer extends Disposable implements IRenderer { this._renderLayers.forEach(l => l.resize(this.dimensions)); // Resize the screen - this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; - this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; + this._screenElement.style.width = `${this.dimensions.canvasWidth}px`; + this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; } public onCharSizeChanged(): void { - this.onResize(this._terminal.cols, this._terminal.rows); + this.onResize(this._bufferService.cols, this._bufferService.rows); } public onBlur(): void { @@ -163,23 +169,23 @@ export class Renderer extends Disposable implements IRenderer { // will be floored because since lineHeight can never be lower then 1, there // is a guarentee that the scaled line height will always be larger than // scaled char height. - this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight); // Calculate the y coordinate within a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + this.dimensions.scaledCharTop = this._optionsService.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); // Calculate the scaled cell width, taking the letterSpacing into account. - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing); // Calculate the x coordinate with a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); + this.dimensions.scaledCharLeft = Math.floor(this._optionsService.options.letterSpacing / 2); // Recalculate the canvas dimensions; scaled* define the actual number of // pixel in the canvas - this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; - this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; + this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight; + this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth; // The the size of the canvas on the page. It's very important that this // rounds to nearest integer and not ceils as browsers often set @@ -194,8 +200,8 @@ export class Renderer extends Disposable implements IRenderer { // account window.devicePixelRatio. ICharSizeService.width/height by itself // is insufficient when the page is not at 100% zoom level as it's measured // in CSS pixels, but the actual char size on the canvas can differ. - this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; - this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; } public registerCharacterJoiner(handler: CharacterJoinerHandler): number { diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 330400ca2..fee261f60 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -227,7 +227,11 @@ export class TextRenderLayer extends BaseRenderLayer { } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { - this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + let bg = cell.getBgColor(); + if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { + bg += 8; + } + this._ctx.fillStyle = this._colors.ansi[bg].css; } } else { if (cell.isFgDefault()) { diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts index 7f84dfd49..b4c321a19 100644 --- a/src/browser/renderer/Types.d.ts +++ b/src/browser/renderer/Types.d.ts @@ -5,6 +5,7 @@ import { IDisposable } from 'common/Types'; import { IColorSet } from 'browser/Types'; +import { IEvent } from 'common/EventEmitter'; export type CharacterJoinerHandler = (text: string) => [number, number][]; @@ -23,6 +24,11 @@ export interface IRenderDimensions { actualCellHeight: number; } +export interface IRequestRefreshRowsEvent { + start: number; + end: number; +} + /** * Note that IRenderer implementations should emit the refresh event after * rendering rows to the screen. @@ -30,6 +36,8 @@ export interface IRenderDimensions { export interface IRenderer extends IDisposable { readonly dimensions: IRenderDimensions; + readonly onRequestRefreshRows: IEvent; + dispose(): void; setColors(colors: IColorSet): void; onDevicePixelRatioChange(): void; diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 40103bc72..f2443a526 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -11,6 +11,7 @@ import { LRUMap } from 'browser/renderer/atlas/LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; import { IColor } from 'browser/Types'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; +import { color } from 'browser/Color'; // 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. @@ -222,7 +223,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { private _getForegroundColor(glyph: IGlyphIdentifier): IColor { if (glyph.fg === INVERTED_DEFAULT_COLOR) { - return this._config.colors.background; + return color.opaque(this._config.colors.background); } else if (glyph.fg < 256) { // 256 color support return this._getColorFromAnsiIndex(glyph.fg); diff --git a/src/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts similarity index 72% rename from src/renderer/dom/DomRenderer.ts rename to src/browser/renderer/dom/DomRenderer.ts index d2c80235f..5fbcfdc9c 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -3,14 +3,15 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; -import { ITerminal } from '../../Types'; +import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } 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 } from 'browser/Types'; +import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; -import { IOptionsService } from 'common/services/Services'; +import { IOptionsService, IBufferService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { color } from 'browser/Color'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -30,19 +31,26 @@ export class DomRenderer extends Disposable implements IRenderer { private _rowFactory: DomRendererRowFactory; private _terminalClass: number = nextTerminalId++; - private _themeStyleElement: HTMLStyleElement; - private _dimensionsStyleElement: HTMLStyleElement; + private _themeStyleElement!: HTMLStyleElement; + private _dimensionsStyleElement!: HTMLStyleElement; private _rowContainer: HTMLElement; private _rowElements: HTMLElement[] = []; private _selectionContainer: HTMLElement; public dimensions: IRenderDimensions; + private _onRequestRefreshRows = new EventEmitter(); + public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } + constructor( - private _terminal: ITerminal, private _colors: IColorSet, - private _charSizeService: ICharSizeService, - private _optionsService: IOptionsService + private readonly _element: HTMLElement, + private readonly _screenElement: HTMLElement, + private readonly _viewportElement: HTMLElement, + private readonly _linkifier: ILinkifier, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IOptionsService private readonly _optionsService: IOptionsService, + @IBufferService private readonly _bufferService: IBufferService ) { super(); @@ -50,60 +58,60 @@ export class DomRenderer extends Disposable implements IRenderer { this._rowContainer.classList.add(ROW_CONTAINER_CLASS); this._rowContainer.style.lineHeight = 'normal'; this._rowContainer.setAttribute('aria-hidden', 'true'); - this._refreshRowElements(this._terminal.cols, this._terminal.rows); + this._refreshRowElements(this._bufferService.cols, this._bufferService.rows); this._selectionContainer = document.createElement('div'); this._selectionContainer.classList.add(SELECTION_CLASS); this._selectionContainer.setAttribute('aria-hidden', 'true'); this.dimensions = { - scaledCharWidth: null, - scaledCharHeight: null, - scaledCellWidth: null, - scaledCellHeight: null, - scaledCharLeft: null, - scaledCharTop: null, - scaledCanvasWidth: null, - scaledCanvasHeight: null, - canvasWidth: null, - canvasHeight: null, - actualCellWidth: null, - actualCellHeight: null + scaledCharWidth: 0, + scaledCharHeight: 0, + scaledCellWidth: 0, + scaledCellHeight: 0, + scaledCharLeft: 0, + scaledCharTop: 0, + scaledCanvasWidth: 0, + scaledCanvasHeight: 0, + canvasWidth: 0, + canvasHeight: 0, + actualCellWidth: 0, + actualCellHeight: 0 }; this._updateDimensions(); this._injectCss(); - this._rowFactory = new DomRendererRowFactory(document, this._optionsService); + this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors); - this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); - this._terminal.screenElement.appendChild(this._rowContainer); - this._terminal.screenElement.appendChild(this._selectionContainer); + this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); + this._screenElement.appendChild(this._rowContainer); + this._screenElement.appendChild(this._selectionContainer); - this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e)); - this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e)); + this._linkifier.onLinkHover(e => this._onLinkHover(e)); + this._linkifier.onLinkLeave(e => this._onLinkLeave(e)); } public dispose(): void { - this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); - this._terminal.screenElement.removeChild(this._rowContainer); - this._terminal.screenElement.removeChild(this._selectionContainer); - this._terminal.screenElement.removeChild(this._themeStyleElement); - this._terminal.screenElement.removeChild(this._dimensionsStyleElement); + this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); + this._screenElement.removeChild(this._rowContainer); + this._screenElement.removeChild(this._selectionContainer); + this._screenElement.removeChild(this._themeStyleElement); + this._screenElement.removeChild(this._dimensionsStyleElement); super.dispose(); } private _updateDimensions(): void { this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio; this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); - this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight); this.dimensions.scaledCharLeft = 0; this.dimensions.scaledCharTop = 0; - this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols; - this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows; + this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols; + this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows; this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); - this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; - this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; this._rowElements.forEach(element => { element.style.width = `${this.dimensions.canvasWidth}px`; @@ -115,7 +123,7 @@ export class DomRenderer extends Disposable implements IRenderer { if (!this._dimensionsStyleElement) { this._dimensionsStyleElement = document.createElement('style'); - this._terminal.screenElement.appendChild(this._dimensionsStyleElement); + this._screenElement.appendChild(this._dimensionsStyleElement); } const styles = @@ -128,9 +136,9 @@ export class DomRenderer extends Disposable implements IRenderer { this._dimensionsStyleElement.innerHTML = styles; - this._selectionContainer.style.height = (this._terminal)._viewportElement.style.height; - this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; - this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; + this._selectionContainer.style.height = this._viewportElement.style.height; + this._screenElement.style.width = `${this.dimensions.canvasWidth}px`; + this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; } public setColors(colors: IColorSet): void { @@ -141,7 +149,7 @@ export class DomRenderer extends Disposable implements IRenderer { private _injectCss(): void { if (!this._themeStyleElement) { this._themeStyleElement = document.createElement('style'); - this._terminal.screenElement.appendChild(this._themeStyleElement); + this._screenElement.appendChild(this._themeStyleElement); } // Base CSS @@ -149,16 +157,16 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + ` color: ${this._colors.foreground.css};` + ` background-color: ${this._colors.background.css};` + - ` font-family: ${this._terminal.options.fontFamily};` + - ` font-size: ${this._terminal.options.fontSize}px;` + + ` font-family: ${this._optionsService.options.fontFamily};` + + ` font-size: ${this._optionsService.options.fontSize}px;` + `}`; // Text styles styles += `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` + - ` font-weight: ${this._terminal.options.fontWeight};` + + ` font-weight: ${this._optionsService.options.fontWeight};` + `}` + `${this._terminalSelector} span.${BOLD_CLASS} {` + - ` font-weight: ${this._terminal.options.fontWeightBold};` + + ` font-weight: ${this._optionsService.options.fontWeightBold};` + `}` + `${this._terminalSelector} span.${ITALIC_CLASS} {` + ` font-style: italic;` + @@ -183,7 +191,7 @@ export class DomRenderer extends Disposable implements IRenderer { `}`; // Cursor styles += - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + ` outline: 1px solid ${this._colors.cursor.css};` + ` outline-offset: -1px;` + `}` + @@ -197,10 +205,10 @@ export class DomRenderer extends Disposable implements IRenderer { ` background-color: ${this._colors.cursor.css};` + ` color: ${this._colors.cursorAccent.css};` + `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + - ` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + + ` box-shadow: ${this._optionsService.options.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` + `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` + `}`; // Selection @@ -223,7 +231,7 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; }); styles += - `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this._colors.background.css}; }` + + `${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; @@ -242,7 +250,7 @@ export class DomRenderer extends Disposable implements IRenderer { } // Remove excess elements while (this._rowElements.length > rows) { - this._rowContainer.removeChild(this._rowElements.pop()); + this._rowContainer.removeChild(this._rowElements.pop()!); } } @@ -275,13 +283,13 @@ export class DomRenderer extends Disposable implements IRenderer { } // Translate from buffer position to viewport position - const viewportStartRow = start[1] - this._terminal.buffer.ydisp; - const viewportEndRow = end[1] - this._terminal.buffer.ydisp; + const viewportStartRow = start[1] - this._bufferService.buffer.ydisp; + const viewportEndRow = end[1] - this._bufferService.buffer.ydisp; const viewportCappedStartRow = Math.max(viewportStartRow, 0); - const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); + const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1); // No need to draw the selection - if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { + if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) { return; } @@ -295,15 +303,15 @@ export class DomRenderer extends Disposable implements IRenderer { } else { // Draw first row const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; - const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._bufferService.cols; documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); // Draw middle rows const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1; - documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount)); + documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount)); // Draw final row if (viewportCappedStartRow !== viewportCappedEndRow) { // Only draw viewportEndRow if it's not the same as viewporttartRow - const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols; documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); } } @@ -333,7 +341,6 @@ export class DomRenderer extends Disposable implements IRenderer { // Force a refresh this._updateDimensions(); this._injectCss(); - this._terminal.refresh(0, this._terminal.rows - 1); } public clear(): void { @@ -341,20 +348,18 @@ export class DomRenderer extends Disposable implements IRenderer { } public renderRows(start: number, end: number): void { - const terminal = this._terminal; - - const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; - const cursorX = this._terminal.buffer.x; - const cursorBlink = this._terminal.options.cursorBlink; + const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y; + const cursorX = this._bufferService.buffer.x; + const cursorBlink = this._optionsService.options.cursorBlink; for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; rowElement.innerHTML = ''; - const row = y + terminal.buffer.ydisp; - const lineData = terminal.buffer.lines.get(row); - const cursorStyle = terminal.options.cursorStyle; - rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols)); + 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)); } } diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index b76d644d6..b6604b14f 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -11,6 +11,7 @@ import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { MockOptionsService } from 'common/TestUtils.test'; +import { css } from 'browser/Color'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -19,7 +20,30 @@ describe('DomRendererRowFactory', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true })); + rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), { + background: css.toColor('#010101'), + foreground: css.toColor('#020202'), + ansi: [ + // dark: + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), + // bright: + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') + ] + } as any); lineData = createEmptyLineData(2); }); @@ -142,7 +166,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -153,7 +177,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -163,7 +187,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -188,7 +212,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -199,7 +223,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 316645f82..c7c87451e 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -5,10 +5,11 @@ import { IBufferLine } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { AttributeData } from 'common/buffer/AttributeData'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; -import { ITerminalOptions, IOptionsService } from 'common/services/Services'; +import { IOptionsService } from 'common/services/Services'; +import { color, rgba } from 'browser/Color'; +import { IColorSet, IColor } from 'browser/Types'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; @@ -24,11 +25,16 @@ export class DomRendererRowFactory { private _workCell: CellData = new CellData(); constructor( - private _document: Document, - private _optionsService: IOptionsService + private readonly _document: Document, + private readonly _optionsService: IOptionsService, + private _colors: IColorSet ) { } + public setColors(colors: IColorSet): void { + this._colors = colors; + } + public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); @@ -95,38 +101,107 @@ export class DomRendererRowFactory { charElement.classList.add(UNDERLINE_CLASS); } - charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; - - const swapColor = this._workCell.isInverse(); - - // fg - if (this._workCell.isFgRGB()) { - let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._workCell.getFgColor())).join(',')});`; - charElement.setAttribute('style', style); - } else if (this._workCell.isFgPalette()) { - let fg = this._workCell.getFgColor(); - if (this._workCell.isBold() && fg < 8 && !swapColor && this._optionsService.options.drawBoldTextInBrightColors) { - fg += 8; - } - charElement.classList.add(`xterm-${swapColor ? 'b' : 'f'}g-${fg}`); - } else if (swapColor) { - charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); + if (this._workCell.isInvisible()) { + charElement.textContent = WHITESPACE_CELL_CHAR; + } else { + charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; } - // bg - if (this._workCell.isBgRGB()) { - let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._workCell.getBgColor())).join(',')});`; - charElement.setAttribute('style', style); - } else if (this._workCell.isBgPalette()) { - charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._workCell.getBgColor()}`); - } else if (swapColor) { - charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); + 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 (isInverse) { + const temp = fg; + fg = bg; + bg = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; + } + + // Foreground + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) { + fg += 8; + } + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) { + charElement.classList.add(`xterm-fg-${fg}`); + } + break; + case Attributes.CM_RGB: + const color = rgba.toColor( + (fg >> 16) & 0xFF, + (fg >> 8) & 0xFF, + (fg ) & 0xFF + ); + if (!this._applyMinimumContrast(charElement, this._colors.background, color)) { + this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`); + } + break; + case Attributes.CM_DEFAULT: + default: + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) { + if (isInverse) { + charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); + } + } + } + + // Background + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + charElement.classList.add(`xterm-bg-${bg}`); + break; + case Attributes.CM_RGB: + this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`); + break; + case Attributes.CM_DEFAULT: + default: + if (isInverse) { + charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); + } } fragment.appendChild(charElement); } return fragment; } + + private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean { + if (this._optionsService.options.minimumContrastRatio === 1) { + return false; + } + + // Try get from cache first + let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + + // Calculate and store in cache + if (adjustedColor === undefined) { + adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); + this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + } + + if (adjustedColor) { + this._addStyle(element, `color:${adjustedColor.css}`); + return true; + } + + return false; + } + + private _addStyle(element: HTMLElement, style: string): void { + element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`); + } +} + +function padStart(text: string, padChar: string, length: number): string { + while (text.length < length) { + text = padChar + text; + } + return text; } diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts new file mode 100644 index 000000000..e585f44ab --- /dev/null +++ b/src/browser/services/CoreBrowserService.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreBrowserService } from './Services'; + +export class CoreBrowserService implements ICoreBrowserService { + serviceBrand: any; + + constructor( + private _textarea: HTMLTextAreaElement + ) { + } + + public get isFocused(): boolean { + return document.activeElement === this._textarea && document.hasFocus(); + } +} diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 63084f545..2949c2c50 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -51,6 +51,9 @@ export class RenderService extends Disposable implements IRenderService { this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); + // No need to register this as renderer is explicitly disposed in RenderService.dispose + this._renderer.onRequestRefreshRows(e => this.refreshRows(e.start, e.end)); + // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange())); @@ -92,6 +95,7 @@ export class RenderService extends Disposable implements IRenderService { public changeOptions(): void { this._renderer.onOptionsChanged(); + this.refreshRows(0, this._rowCount - 1); this._fireOnCanvasResize(); } @@ -105,12 +109,14 @@ export class RenderService extends Disposable implements IRenderService { public dispose(): void { this._renderer.dispose(); + super.dispose(); } public setRenderer(renderer: IRenderer): void { // TODO: RenderService should be the only one to dispose the renderer this._renderer.dispose(); this._renderer = renderer; + this._renderer.onRequestRefreshRows(e => this.refreshRows(e.start, e.end)); this.refreshRows(0, this._rowCount - 1); } diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 2efb095d6..4eb6550cb 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -210,7 +210,7 @@ export class SelectionService implements ISelectionService { for (let i = start[1] + 1; i <= end[1] - 1; i++) { const bufferLine = buffer.lines.get(i); const lineText = buffer.translateBufferLineToString(i, true); - if (bufferLine!.isWrapped) { + if (bufferLine && bufferLine.isWrapped) { result[result.length - 1] += lineText; } else { result.push(lineText); @@ -221,7 +221,7 @@ export class SelectionService implements ISelectionService { if (start[1] !== end[1]) { const bufferLine = buffer.lines.get(end[1]); const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]); - if (bufferLine!.isWrapped) { + if (bufferLine && bufferLine!.isWrapped) { result[result.length - 1] += lineText; } else { result.push(lineText); @@ -688,9 +688,7 @@ export class SelectionService implements ISelectionService { // reverseIndex) and delete in a splice is only ever used when the same // number of elements was just added. Given this is could actually be // beneficial to leave the selection as is for these cases. - if (this._trimListener) { - this._trimListener.dispose(); - } + this._trimListener.dispose(); this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount)); } diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 2170a04e1..785fec34e 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -23,6 +23,13 @@ export interface ICharSizeService { measure(): void; } +export const ICoreBrowserService = createDecorator('CoreBrowserService'); +export interface ICoreBrowserService { + serviceBrand: any; + + readonly isFocused: boolean; +} + export const IMouseService = createDecorator('MouseService'); export interface IMouseService { serviceBrand: any; diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index 34ac190f1..301574e42 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -5,33 +5,37 @@ import { IDisposable } from 'common/Types'; -interface IListener { - (e: T): void; +interface IListener { + (arg1: T, arg2: U): void; } -export interface IEvent { - (listener: (e: T) => any): IDisposable; +export interface IEvent { + (listener: (arg1: T, arg2: U) => any): IDisposable; } -export interface IEventEmitter { - event: IEvent; - fire(data: T): void; +export interface IEventEmitter { + event: IEvent; + fire(arg1: T, arg2: U): void; + dispose(): void; } -export class EventEmitter implements IEventEmitter { - private _listeners: IListener[] = []; - private _event?: IEvent; +export class EventEmitter implements IEventEmitter { + private _listeners: IListener[] = []; + private _event?: IEvent; + private _disposed: boolean = false; - public get event(): IEvent { + public get event(): IEvent { if (!this._event) { - this._event = (listener: (e: T) => any) => { + this._event = (listener: (arg1: T, arg2: U) => any) => { this._listeners.push(listener); const disposable = { dispose: () => { - for (let i = 0; i < this._listeners.length; i++) { - if (this._listeners[i] === listener) { - this._listeners.splice(i, 1); - return; + if (!this._disposed) { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] === listener) { + this._listeners.splice(i, 1); + return; + } } } } @@ -42,13 +46,20 @@ export class EventEmitter implements IEventEmitter { return this._event; } - public fire(data: T): void { - const queue: IListener[] = []; + public fire(arg1: T, arg2: U): void { + const queue: IListener[] = []; for (let i = 0; i < this._listeners.length; i++) { queue.push(this._listeners[i]); } for (let i = 0; i < queue.length; i++) { - queue[i].call(undefined, data); + queue[i].call(undefined, arg1, arg2); } } + + public dispose(): void { + if (this._listeners) { + this._listeners.length = 0; + } + this._disposed = true; + } } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index f2721bbe9..581477481 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 } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService } 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 } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset } from 'common/Types'; export class MockBufferService implements IBufferService { serviceBrand: any; @@ -42,13 +42,33 @@ export class MockCoreMouseService implements ICoreMouseService { } } +export class MockCharsetService implements ICharsetService { + serviceBrand: any; + charset: ICharset | undefined; + glevel: number = 0; + charsets: readonly ICharset[] = []; + reset(): void {} + setgLevel(g: number): void {} + setgCharset(g: number, charset: ICharset): void {} +} + export class MockCoreService implements ICoreService { serviceBrand: any; - decPrivateModes: IDecPrivateModes = {} as any; + isCursorInitialized: boolean = false; + isCursorHidden: boolean = false; + isFocused: boolean = false; + decPrivateModes: IDecPrivateModes = { + applicationCursorKeys: false, + applicationKeypad: false, + origin: false, + wraparound: true + }; onData: IEvent = new EventEmitter().event; onUserInput: IEvent = new EventEmitter().event; + onBinary: IEvent = new EventEmitter().event; reset(): void {} triggerDataEvent(data: string, wasUserInput?: boolean): void {} + triggerBinaryEvent(data: string): void {} } export class MockDirtyRowService implements IDirtyRowService { diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 2f7420389..fdb5eb739 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -62,7 +62,7 @@ export interface IKeyboardResult { } export interface ICharset { - [key: string]: string; + [key: string]: string | undefined; } export type CharData = [number, string, number, number]; @@ -123,9 +123,9 @@ export interface IBufferLine { setCell(index: number, cell: ICellData): void; setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; addCodepointToCell(index: number, codePoint: number): void; - insertCells(pos: number, n: number, ch: ICellData): void; - deleteCells(pos: number, n: number, fill: ICellData): void; - replaceCells(start: number, end: number, fill: ICellData): void; + insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; + deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void; + replaceCells(start: number, end: number, fill: ICellData, eraseAttr?: IAttributeData): void; resize(cols: number, fill: ICellData): void; fill(fillCellData: ICellData): void; copyFrom(line: IBufferLine): void; @@ -152,6 +152,9 @@ export interface IMarker extends IDisposable { export interface IDecPrivateModes { applicationCursorKeys: boolean; + applicationKeypad: boolean; + origin: boolean; + wraparound: boolean; // defaults: xterm - true, vt100 - false } export interface IRowRange { @@ -249,5 +252,8 @@ export interface ICoreMouseProtocol { * The tracking encoding can be registered and activated at the CoreMouseService. * If a ICoreMouseEvent passes all procotol restrictions it will be encoded * with the active encoding and sent out. + * Note: Returning an empty string will supress sending a mouse report, + * which can be used to skip creating falsey reports in limited encodings + * (DEFAULT only supports up to 223 1-based as coord value). */ export type CoreMouseEncoding = (event: ICoreMouseEvent) => string; diff --git a/src/WindowsMode.ts b/src/common/WindowsMode.ts similarity index 67% rename from src/WindowsMode.ts rename to src/common/WindowsMode.ts index 0838cae20..ff0e75913 100644 --- a/src/WindowsMode.ts +++ b/src/common/WindowsMode.ts @@ -3,11 +3,10 @@ * @license MIT */ -import { IDisposable } from 'xterm'; -import { ITerminal } from './Types'; import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { IBufferService } from 'common/services/Services'; -export function applyWindowsMode(terminal: ITerminal): IDisposable { +export function handleWindowsModeLineFeed(bufferService: IBufferService): void { // Winpty does not support wraparound mode which means that lines will never // be marked as wrapped. This causes issues for things like copying a line // retaining the wrapped new line characters or if consumers are listening @@ -18,11 +17,11 @@ export function applyWindowsMode(terminal: ITerminal): IDisposable { // space. This is certainly not without its problems, but generally on // Windows when text reaches the end of the terminal it's likely going to be // wrapped. - return terminal.onLineFeed(() => { - const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); - const lastChar = line.get(terminal.cols - 1); + const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1); + const lastChar = line?.get(bufferService.cols - 1); - const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y); + const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y); + if (nextLine && lastChar) { nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE); - }); + } } diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 1b3e5d861..152bab7a3 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -167,19 +167,25 @@ export class Buffer implements IBuffer { if (this._rows < newRows) { for (let y = this._rows; y < newRows; y++) { if (this.lines.length < newRows + this.ybase) { - if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { - // There is room above the buffer and there are no empty elements below the line, - // scroll up - this.ybase--; - addToY++; - if (this.ydisp > 0) { - // Viewport is at the top of the buffer, must increase downwards - this.ydisp--; - } - } else { - // Add a blank line if there is no buffer left at the top to scroll to, or if there - // are blank lines after the cursor + if (this._optionsService.options.windowsMode) { + // Just add the new missing rows on Windows as conpty reprints the screen with it's + // view of the world. Once a line enters scrollback for conpty it remains there this.lines.push(new BufferLine(newCols, nullCell)); + } else { + if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { + // There is room above the buffer and there are no empty elements below the line, + // scroll up + this.ybase--; + addToY++; + if (this.ydisp > 0) { + // Viewport is at the top of the buffer, must increase downwards + this.ydisp--; + } + } else { + // Add a blank line if there is no buffer left at the top to scroll to, or if there + // are blank lines after the cursor + this.lines.push(new BufferLine(newCols, nullCell)); + } } } } diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index ae80aa16f..686371f25 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -366,4 +366,94 @@ describe('BufferLine', function(): void { chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); }); + describe('correct fullwidth handling', () => { + function populate(line: BufferLine): void { + const cell = CellData.fromCharData([1, '¥', 2, '¥'.charCodeAt(0)]); + for (let i = 0; i < line.length; i += 2) { + line.setCell(i, cell); + } + } + it('insert - wide char at pos', () => { + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.insertCells(9, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), '¥¥¥¥ a'); + line.insertCells(8, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), '¥¥¥¥a '); + line.insertCells(1, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' a ¥¥¥a'); + }); + it('insert - wide char at end', () => { + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.insertCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaa¥¥¥ '); + line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaa a ¥¥'); + line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaa aa ¥ '); + }); + it('delete', () => { + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.deleteCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' ¥¥¥¥a'); + line.deleteCells(5, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' ¥¥¥aaa'); + line.deleteCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' ¥¥aaaaa'); + }); + it('replace - start at 0', () => { + let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'a ¥¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aa¥¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaa ¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaaaaaaa¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaaaaaaaa '); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaaaaaaaaa'); + }); + it('replace - start at 1', () => { + let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' a¥¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aa ¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aaa¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aaaaaaa¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aaaaaaaa '); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aaaaaaaaa'); + }); + }); }); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 8e742be38..d54b59aaf 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { CharData, IBufferLine, ICellData } from 'common/Types'; +import { CharData, IBufferLine, ICellData, IAttributeData } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; -import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; @@ -228,8 +228,14 @@ export class BufferLine implements IBufferLine { } } - public insertCells(pos: number, n: number, fillCellData: ICellData): void { + public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { pos %= this.length; + + // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } + if (n < this.length - pos) { const cell = new CellData(); for (let i = this.length - pos - n - 1; i >= 0; --i) { @@ -243,9 +249,14 @@ export class BufferLine implements IBufferLine { this.setCell(i, fillCellData); } } + + // handle fullwidth at line end: reset last cell if it is first cell of a wide char + if (this.getWidth(this.length - 1) === 2) { + this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } } - public deleteCells(pos: number, n: number, fillCellData: ICellData): void { + public deleteCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { pos %= this.length; if (n < this.length - pos) { const cell = new CellData(); @@ -260,9 +271,28 @@ export class BufferLine implements IBufferLine { this.setCell(i, fillCellData); } } + + // handle fullwidth at pos: + // - reset pos-1 if wide char + // - reset pos if width==0 (previous second cell of a wide char) + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } + if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { + this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } } - public replaceCells(start: number, end: number, fillCellData: ICellData): void { + public replaceCells(start: number, end: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char + if (start && this.getWidth(start - 1) === 2) { + this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } + // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char + if (end < this.length && this.getWidth(end - 1) === 2) { + this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } + while (start < end && start < this.length) { this.setCell(start++, fillCellData); } diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts index 7f52ac99c..9a4256d37 100644 --- a/src/common/buffer/Marker.ts +++ b/src/common/buffer/Marker.ts @@ -29,6 +29,7 @@ export class Marker extends Disposable implements IMarker { return; } this.isDisposed = true; + this.line = -1; // Emit before super.dispose such that dispose listeners get a change to react this._onDispose.fire(); } diff --git a/src/common/input/Keyboard.test.ts b/src/common/input/Keyboard.test.ts index 409a31926..a304923e8 100644 --- a/src/common/input/Keyboard.test.ts +++ b/src/common/input/Keyboard.test.ts @@ -108,6 +108,12 @@ describe('Keyboard', () => { it('should return \\x1b[5C for alt+right', () => { assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C }); + it('should return \\x1b[5D for alt+up', () => { + assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;5A'); // CSI 5 D + }); + it('should return \\x1b[5C for alt+down', () => { + assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;5B'); // CSI 5 C + }); it('should return \\x1ba for alt+a', () => { assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba'); }); @@ -120,6 +126,12 @@ describe('Keyboard', () => { it('should return \\x1bf for alt+right', () => { assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C }); + it('should return \\x1bb for alt+up', () => { + assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 5 D + }); + it('should return \\x1bf for alt+down', () => { + assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 5 C + }); it('should return undefined for alt+a', () => { assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true }; }); diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index 2f54add6b..1bf378c17 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -113,13 +113,16 @@ export function evaluateKeyboardEvent( break; case 37: // left-arrow + if (ev.metaKey) { + break; + } if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D'; // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards // http://unix.stackexchange.com/a/108106 // macOS uses different escape sequences than linux if (result.key === C0.ESC + '[1;3D') { - result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D'; + result.key = C0.ESC + (isMac ? 'b' : '[1;5D'); } } else if (applicationCursorMode) { result.key = C0.ESC + 'OD'; @@ -129,13 +132,16 @@ export function evaluateKeyboardEvent( break; case 39: // right-arrow + if (ev.metaKey) { + break; + } if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C'; // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward // http://unix.stackexchange.com/a/108106 // macOS uses different escape sequences than linux if (result.key === C0.ESC + '[1;3C') { - result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C'; + result.key = C0.ESC + (isMac ? 'f' : '[1;5C'); } } else if (applicationCursorMode) { result.key = C0.ESC + 'OC'; @@ -145,11 +151,15 @@ export function evaluateKeyboardEvent( break; case 38: // up-arrow + if (ev.metaKey) { + break; + } if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow // http://unix.stackexchange.com/a/108106 - if (result.key === C0.ESC + '[1;3A') { + // macOS uses different escape sequences than linux + if (!isMac && result.key === C0.ESC + '[1;3A') { result.key = C0.ESC + '[1;5A'; } } else if (applicationCursorMode) { @@ -160,11 +170,15 @@ export function evaluateKeyboardEvent( break; case 40: // down-arrow + if (ev.metaKey) { + break; + } if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow // http://unix.stackexchange.com/a/108106 - if (result.key === C0.ESC + '[1;3B') { + // macOS uses different escape sequences than linux + if (!isMac && result.key === C0.ESC + '[1;3B') { result.key = C0.ESC + '[1;5B'; } } else if (applicationCursorMode) { diff --git a/src/common/input/TextDecoder.test.ts b/src/common/input/TextDecoder.test.ts index cda74a186..92b0e03a4 100644 --- a/src/common/input/TextDecoder.test.ts +++ b/src/common/input/TextDecoder.test.ts @@ -7,6 +7,7 @@ import { assert } from 'chai'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'common/input/TextDecoder'; import { encode } from 'utf8'; + // convert UTF32 codepoints to string function toString(data: Uint32Array, length: number): string { if ((String as any).fromCodePoint) { @@ -214,6 +215,16 @@ describe('text encodings', () => { } assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); }); + it('test break after 3 bytes - issue #2495', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + const utf8Data = fromByteString('\xf0\xa0\x9c\x8e'); + let written = decoder.decode(utf8Data.slice(0, 3), target); + assert.equal(written, 0); + written = decoder.decode(utf8Data.slice(3), target); + assert.equal(written, 1); + assert(toString(target, written), '𠜎'); + }); }); }); }); diff --git a/src/common/input/TextDecoder.ts b/src/common/input/TextDecoder.ts index 7e141e02e..397d25a78 100644 --- a/src/common/input/TextDecoder.ts +++ b/src/common/input/TextDecoder.ts @@ -194,7 +194,7 @@ export class Utf8ToUtf32 { target[size++] = cp; } } else { - if (codepoint < 0x010000 || codepoint > 0x10FFFF) { + if (cp < 0x010000 || cp > 0x10FFFF) { // illegal codepoint } else { target[size++] = cp; diff --git a/src/common/services/CharsetService.ts b/src/common/services/CharsetService.ts new file mode 100644 index 000000000..5d20628f3 --- /dev/null +++ b/src/common/services/CharsetService.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharsetService } from 'common/services/Services'; +import { ICharset } from 'common/Types'; + +export class CharsetService implements ICharsetService { + serviceBrand: any; + + public charset: ICharset | undefined; + public charsets: ICharset[] = []; + public glevel: number = 0; + + public reset(): void { + this.charset = undefined; + this.charsets = []; + this.glevel = 0; + } + + public setgLevel(g: number): void { + this.glevel = g; + this.charset = this.charsets[g]; + } + + public setgCharset(g: number, charset: ICharset): void { + this.charsets[g] = charset; + if (this.glevel === g) { + this.charset = charset; + } + } +} diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index 2063a496a..2bf011b02 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -32,9 +32,9 @@ describe('CoreMouseService', () => { const cms = new CoreMouseService(bufferService, coreService); assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']); }); - it('default encodings - DEFAULT, UTF8, SGR, URXVT', () => { + it('default encodings - DEFAULT, SGR', () => { const cms = new CoreMouseService(bufferService, coreService); - assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'UTF8', 'SGR', 'URXVT']); + assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR']); }); it('protocol/encoding setter, reset', () => { const cms = new CoreMouseService(bufferService, coreService); @@ -79,6 +79,7 @@ describe('CoreMouseService', () => { cms = new CoreMouseService(bufferService, coreService); reports = []; coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); + coreService.triggerBinaryEvent = (data: string) => reports.push(data); }); it('NONE', () => { assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); @@ -143,22 +144,14 @@ describe('CoreMouseService', () => { cms.activeProtocol = 'ANY'; for (let i = 0; i < bufferService.cols; ++i) { assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - // capped at 95 - if (i < 95) { - assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); + if (i > 222) { + // supress mouse reports if we are out of addressible range (max. 222) + assert.deepEqual(toBytes(reports.pop()), []); } else { - assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, 0x7f, 0x21]); + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); } } }); - it('UTF8 encoding', () => { - cms.activeProtocol = 'ANY'; - cms.activeEncoding = 'UTF8'; - for (let i = 0; i < bufferService.cols; ++i) { - assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); - } - }); it('SGR encoding', () => { cms.activeProtocol = 'ANY'; cms.activeEncoding = 'SGR'; @@ -167,14 +160,6 @@ describe('CoreMouseService', () => { assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`); } }); - it('URXVT', () => { - cms.activeProtocol = 'ANY'; - cms.activeEncoding = 'URXVT'; - for (let i = 0; i < bufferService.cols; ++i) { - assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - assert.deepEqual(reports.pop(), `\x1b[32;${i + 1};1M`); - } - }); }); it('eventCodes with modifiers (DEFAULT encoding)', () => { // TODO: implement AUX button tests diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 0c8465192..0bd25dd03 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -121,26 +121,17 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { /** * DEFAULT - CSI M Pb Px Py * Single byte encoding for coords and event code. - * Can encode values up to 223. The Encoding of higher - * values is not UTF-8 compatible (and currently limited - * to 95 in xterm.js). + * Can encode values up to 223 (1-based). */ DEFAULT: (e: ICoreMouseEvent) => { - let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; - // FIXME: we are currently limited to ASCII range - params = params.map(v => (v > 127) ? 127 : v); - // FIXED: params = params.map(v => (v > 255) ? 0 : value); - return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; - }, - /** - * UTF8 - CSI M Pb Px Py - * Same as DEFAULT, but with optional 2-byte UTF8 - * encoding for values > 223 (can encode up to 2015). - */ - UTF8: (e: ICoreMouseEvent) => { - let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; - // limit to 2-byte UTF8 - params = params.map(v => (v > 2047) ? 0 : v); + const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + // supress mouse report if we exceed addressible range + // Note this is handled differently by emulators + // - xterm: sends 0;0 coords instead + // - vte, konsole: no report + if (params[0] > 255 || params[1] > 255 || params[2] > 255) { + return ''; + } return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; }, /** @@ -151,14 +142,6 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { SGR: (e: ICoreMouseEvent) => { const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M'; return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`; - }, - /** - * URXVT - CSI Pb ; Px ; Py M - * Same button encoding as default, decimal encoding for coords. - * Ambiguity with other sequences, should not be used. - */ - URXVT: (e: ICoreMouseEvent) => { - return `\x1b[${eventCode(e, false) + 32};${e.col};${e.row}M`; } }; @@ -167,7 +150,7 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { * * Provides mouse tracking reports with different protocols and encodings. * - protocols: NONE (default), X10, VT200, DRAG, ANY - * - encodings: DEFAULT, SGR, UTF8, URXVT + * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507) * * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`. * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`. @@ -285,7 +268,14 @@ export class CoreMouseService implements ICoreMouseService { // encode report and send const report = this._encodings[this._activeEncoding](e); - this._coreService.triggerDataEvent(report, true); + if (report) { + // always send DEFAULT as binary data + if (this._activeEncoding === 'DEFAULT') { + this._coreService.triggerBinaryEvent(report); + } else { + this._coreService.triggerDataEvent(report, true); + } + } this._lastEvent = e; diff --git a/src/common/services/CoreService.test.ts b/src/common/services/CoreService.test.ts new file mode 100644 index 000000000..00be49538 --- /dev/null +++ b/src/common/services/CoreService.test.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreService } from 'common/services/Services'; +import { CoreService } from 'common/services/CoreService'; +import { MockBufferService, MockLogService, MockOptionsService } from 'common/TestUtils.test'; +import { assert } from 'chai'; + +describe('CoreService', () => { + let coreService: ICoreService; + + beforeEach(() => { + coreService = new CoreService( + () => {}, + new MockBufferService(80, 30), + new MockLogService(), + new MockOptionsService()); + }); + + describe('reset', () => { + it('should not affect isCursorInitialized', () => { + coreService.isCursorInitialized = true; + coreService.reset(); + assert.equal(coreService.isCursorInitialized, true); + coreService.isCursorInitialized = false; + coreService.reset(); + assert.equal(coreService.isCursorInitialized, false); + }); + }); +}); diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 0e0ba6091..df9161e9c 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -5,22 +5,29 @@ import { ICoreService, ILogService, IOptionsService, IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IDecPrivateModes } from 'common/Types'; +import { IDecPrivateModes, ICharset } from 'common/Types'; import { clone } from 'common/Clone'; const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ - applicationCursorKeys: false + applicationCursorKeys: false, + applicationKeypad: false, + origin: false, + wraparound: true // defaults: xterm - true, vt100 - false }); export class CoreService implements ICoreService { serviceBrand: any; + public isCursorInitialized: boolean = false; + public isCursorHidden: boolean = false; public decPrivateModes: IDecPrivateModes; private _onData = new EventEmitter(); public get onData(): IEvent { return this._onData.event; } private _onUserInput = new EventEmitter(); public get onUserInput(): IEvent { return this._onUserInput.event; } + private _onBinary = new EventEmitter(); + public get onBinary(): IEvent { return this._onBinary.event; } constructor( // TODO: Move this into a service @@ -57,4 +64,12 @@ export class CoreService implements ICoreService { this._logService.debug(`sending data "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); this._onData.fire(data); } + + public triggerBinaryEvent(data: string): void { + if (this._optionsService.options.disableStdin) { + return; + } + this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); + this._onBinary.fire(data); + } } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 9a5d2151d..7cd111ac0 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -12,7 +12,7 @@ import { clone } from 'common/Clone'; // This sound is released under the Creative Commons Attribution 3.0 Unported // (CC BY 3.0) license. It was created by 'altemark'. No modifications have been // made, apart from the conversion to base64. -export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; +export const DEFAULT_BELL_SOUND = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; // TODO: Freeze? export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ @@ -20,9 +20,12 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ rows: 24, cursorBlink: false, cursorStyle: 'block', + cursorWidth: 1, bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', drawBoldTextInBrightColors: true, + fastScrollModifier: 'alt', + fastScrollSensitivity: 5, fontFamily: 'courier-new, courier, monospace', fontSize: 15, fontWeight: 'normal', @@ -31,9 +34,11 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ letterSpacing: 0, logLevel: 'info', scrollback: 1000, + scrollSensitivity: 1, screenReaderMode: false, macOptionIsMeta: false, macOptionClickForcesSelection: false, + minimumContrastRatio: 1, disableStdin: false, allowTransparency: false, tabStopWidth: 8, @@ -47,7 +52,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenKeys: false, cancelEvents: false, useFlowControl: false, - wordSeparator: ' ()[]{}\'"' + wordSeparator: ' ()[]{}\',:;"`' }); /** @@ -107,18 +112,30 @@ export class OptionsService implements IOptionsService { value = DEFAULT_OPTIONS[key]; } break; + case 'cursorWidth': + value = Math.floor(value); + // Fall through for bounds check case 'lineHeight': case 'tabStopWidth': if (value < 1) { throw new Error(`${key} cannot be less than 1, value: ${value}`); } break; + case 'minimumContrastRatio': + value = Math.max(1, Math.min(21, Math.round(value * 10) / 10)); + break; case 'scrollback': value = Math.min(value, 4294967295); if (value < 0) { throw new Error(`${key} cannot be less than 0, value: ${value}`); } break; + case 'fastScrollSensitivity': + case 'scrollSensitivity': + if (value <= 0) { + throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`); + } + break; } return value; } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 568eeaffe..32274f5e9 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 } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -58,10 +58,18 @@ export const ICoreService = createDecorator('CoreService'); export interface ICoreService { serviceBrand: any; + /** + * Initially the cursor will not be visible until the first time the terminal + * is focused. + */ + isCursorInitialized: boolean; + isCursorHidden: boolean; + readonly decPrivateModes: IDecPrivateModes; readonly onData: IEvent; readonly onUserInput: IEvent; + readonly onBinary: IEvent; reset(): void; @@ -72,8 +80,38 @@ export interface ICoreService { * resulting from parsing incoming data). When true this will also: * - Scroll to the bottom of the buffer.s * - Fire the `onUserInput` event (so selection can be cleared). - */ + */ triggerDataEvent(data: string, wasUserInput?: boolean): void; + + /** + * Triggers the onBinary event in the public API. + * @param data The data that is being emitted. + */ + triggerBinaryEvent(data: string): void; +} + +export const ICharsetService = createDecorator('CharsetService'); +export interface ICharsetService { + serviceBrand: any; + + charset: ICharset | undefined; + readonly glevel: number; + readonly charsets: ReadonlyArray; + + reset(): void; + + /** + * Set the G level of the terminal. + * @param g + */ + setgLevel(g: number): void; + + /** + * Set the charset for the given G level of the terminal. + * @param g + * @param charset + */ + setgCharset(g: number, charset: ICharset): void; } export const IDirtyRowService = createDecorator('DirtyRowService'); @@ -178,8 +216,11 @@ export interface IPartialTerminalOptions { cols?: number; cursorBlink?: boolean; cursorStyle?: 'block' | 'underline' | 'bar'; + cursorWidth?: number; disableStdin?: boolean; drawBoldTextInBrightColors?: boolean; + fastScrollModifier?: 'alt' | 'ctrl' | 'shift'; + fastScrollSensitivity?: number; fontSize?: number; fontFamily?: string; fontWeight?: FontWeight; @@ -194,6 +235,7 @@ export interface IPartialTerminalOptions { rows?: number; screenReaderMode?: boolean; scrollback?: number; + scrollSensitivity?: number; tabStopWidth?: number; theme?: ITheme; windowsMode?: boolean; @@ -207,8 +249,11 @@ export interface ITerminalOptions { cols: number; cursorBlink: boolean; cursorStyle: 'block' | 'underline' | 'bar'; + cursorWidth: number; disableStdin: boolean; drawBoldTextInBrightColors: boolean; + fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined; + fastScrollSensitivity: number; fontSize: number; fontFamily: string; fontWeight: FontWeight; @@ -218,11 +263,13 @@ export interface ITerminalOptions { logLevel: LogLevel; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; + minimumContrastRatio: number; rendererType: RendererType; rightClickSelectsWord: boolean; rows: number; screenReaderMode: boolean; scrollback: number; + scrollSensitivity: number; tabStopWidth: number; theme: ITheme; windowsMode: boolean; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index b8a70ff7e..96e13f634 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -27,20 +27,21 @@ export class Terminal implements ITerminalApi { 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 onBinary(): IEvent { return this._core.onBinary; } public get onTitleChange(): IEvent { return this._core.onTitleChange; } public get onScroll(): IEvent { return this._core.onScroll; } public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } 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 element(): HTMLElement { return this._core.element; } + public get element(): HTMLElement | undefined { return this._core.element; } public get parser(): IParser { if (!this._parser) { this._parser = new ParserApi(this._core); } return this._parser; } - public get textarea(): HTMLTextAreaElement { return this._core.textarea; } + public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; } public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } public get buffer(): IBufferApi { return new BufferApiView(this._core.buffer); } @@ -73,10 +74,13 @@ export class Terminal implements ITerminalApi { public deregisterCharacterJoiner(joinerId: number): void { this._core.deregisterCharacterJoiner(joinerId); } - public addMarker(cursorYOffset: number): IMarker { + public registerMarker(cursorYOffset: number): IMarker { this._verifyIntegers(cursorYOffset); return this._core.addMarker(cursorYOffset); } + public addMarker(cursorYOffset: number): IMarker { + return this.registerMarker(cursorYOffset); + } public hasSelection(): boolean { return this._core.hasSelection(); } @@ -178,8 +182,8 @@ export class Terminal implements ITerminalApi { private _verifyIntegers(...values: number[]): void { values.forEach(value => { - if (value % 1 !== 0) { - throw new Error('This API does not accept floating point numbers'); + if (value === Infinity || isNaN(value) || value % 1 !== 0) { + throw new Error('This API only accepts integers'); } }); } @@ -226,16 +230,28 @@ class BufferCellApiView implements IBufferCellApi { class ParserApi implements IParser { constructor(private _core: ITerminal) {} - public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + 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 addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + 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 addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + 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); + } } diff --git a/test/api/CharWidth.api.ts b/test/api/CharWidth.api.ts index 035208ccc..39f0bfe72 100644 --- a/test/api/CharWidth.api.ts +++ b/test/api/CharWidth.api.ts @@ -4,8 +4,8 @@ */ import * as puppeteer from 'puppeteer'; -import { assert } from 'chai'; import { ITerminalOptions } from 'xterm'; +import { pollFor } from './TestUtils'; const APP = 'http://127.0.0.1:3000/test'; @@ -15,13 +15,10 @@ const width = 800; const height = 600; describe('CharWidth Integration Tests', function(): void { - this.timeout(20000); - before(async function(): Promise { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); @@ -41,37 +38,37 @@ describe('CharWidth Integration Tests', function(): void { it('ASCII chars', async function(): Promise { const input = 'This is just ASCII text.#'; await page.evaluate(`window.term.write('${input}')`); - assert.equal(25, await sumWidths(0, 1, '#')); + await pollFor(page, () => sumWidths(0, 1, '#'), 25); }); it('combining chars', async function(): Promise { const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#'; await page.evaluate(`window.term.write('${input}')`); - assert.equal(10, await sumWidths(0, 1, '#')); + await pollFor(page, () => sumWidths(0, 1, '#'), 10); }); it('surrogate chars', async function(): Promise { const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#'; await page.evaluate(`window.term.write('${input}')`); - assert.equal(28, await sumWidths(0, 1, '#')); + await pollFor(page, () => sumWidths(0, 1, '#'), 28); }); it('surrogate combining chars', async function(): Promise { const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#'; await page.evaluate(`window.term.write('${input}')`); - assert.equal(12, await sumWidths(0, 1, '#')); + await pollFor(page, () => sumWidths(0, 1, '#'), 12); }); it('fullwidth chars', async function(): Promise { const input = '1234567890#'; await page.evaluate(`window.term.write('${input}')`); - assert.equal(21, await sumWidths(0, 1, '#')); + await pollFor(page, () => sumWidths(0, 1, '#'), 21); }); it('fullwidth chars offset 1', async function(): Promise { const input = 'a1234567890#'; await page.evaluate(`window.term.write('${input}')`); - assert.equal(22, await sumWidths(0, 1, '#')); + await pollFor(page, () => sumWidths(0, 1, '#'), 22); }); // TODO: multiline tests once #1685 is resolved diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 05b16010c..968a9e0bb 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -6,6 +6,7 @@ import * as puppeteer from 'puppeteer'; import { assert } from 'chai'; import { ITerminalOptions } from 'xterm'; +import { pollFor } from './TestUtils'; const APP = 'http://127.0.0.1:3000/test'; @@ -15,13 +16,10 @@ const width = 800; const height = 600; describe('InputHandler Integration Tests', function(): void { - this.timeout(20000); - before(async function(): Promise { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); @@ -45,7 +43,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('bar\\x1b[3D\\x1b[4@') `); - assert.deepEqual(await getLinesAsArray(2), [' foo', ' bar']); + await pollFor(page, () => getLinesAsArray(2), [' foo', ' bar']); }); it('CUU: Cursor Up Ps Times (default = 1) - CSI Ps A', async function(): Promise { @@ -55,7 +53,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[2Ab') `); - assert.deepEqual(await getLinesAsArray(4), ['', ' b', '', 'a']); + await pollFor(page, () => getLinesAsArray(4), ['', ' b', '', 'a']); }); it('CUD: Cursor Down Ps Times (default = 1) - CSI Ps B', async function(): Promise { @@ -65,7 +63,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[2Bb') `); - assert.deepEqual(await getLinesAsArray(4), ['', 'a', '', ' b']); + await pollFor(page, () => getLinesAsArray(4), ['', 'a', '', ' b']); }); it('CUF: Cursor Forward Ps Times (default = 1) - CSI Ps C', async function(): Promise { @@ -75,7 +73,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[2Cb') `); - assert.deepEqual(await getLinesAsArray(1), [' a b']); + await pollFor(page, () => getLinesAsArray(1), [' a b']); }); it('CUB: Cursor Backward Ps Times (default = 1) - CSI Ps D', async function(): Promise { @@ -85,7 +83,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[2Db') `); - assert.deepEqual(await getLinesAsArray(1), ['fba']); + await pollFor(page, () => getLinesAsArray(1), ['fba']); }); it('CNL: Cursor Next Line Ps Times (default = 1) - CSI Ps E', async function(): Promise { @@ -95,7 +93,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[2Eb') `); - assert.deepEqual(await getLinesAsArray(4), ['', 'a', '', 'b']); + await pollFor(page, () => getLinesAsArray(4), ['', 'a', '', 'b']); }); it('CPL: Cursor Preceding Line Ps Times (default = 1) - CSI Ps F', async function(): Promise { @@ -105,7 +103,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[2Fb') `); - assert.deepEqual(await getLinesAsArray(5), ['', 'b', '', 'a', '']); + await pollFor(page, () => getLinesAsArray(5), ['', 'b', '', 'a', '']); }); it('CHA: Cursor Character Absolute [column] (default = [row,1]) - CSI Ps G', async function(): Promise { @@ -115,7 +113,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[10Gb') `); - assert.deepEqual(await getLinesAsArray(1), ['aoo b']); + await pollFor(page, () => getLinesAsArray(1), ['aoo b']); }); it('CUP: Cursor Position [row;column] (default = [1,1]) - CSI Ps ; Ps H', async function(): Promise { @@ -125,7 +123,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[3;3Hb') `); - assert.deepEqual(await getLinesAsArray(3), ['aoo', '', ' b']); + await pollFor(page, () => getLinesAsArray(3), ['aoo', '', ' b']); }); it('CHT: Cursor Forward Tabulation Ps tab stops (default = 1) - CSI Ps I', async function(): Promise { @@ -135,7 +133,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\\n\\r\x1b[2Ib') `); - assert.deepEqual(await getLinesAsArray(2), [' a', ' b']); + await pollFor(page, () => getLinesAsArray(2), [' a', ' b']); }); it('ED: Erase in Display, VT100 - CSI Ps J', async function(): Promise { @@ -145,26 +143,26 @@ describe('InputHandler Integration Tests', function(): void { window.term.resize(5, 5); window.term.write('${fixture}\x1b[J') `); - assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']); + await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']); await page.evaluate(` // 0: Erase Below window.term.reset() window.term.write('${fixture}\x1b[0J') `); - assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']); + await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']); await page.evaluate(` // 1: Erase Above window.term.reset() window.term.write('${fixture}\x1b[1J') `); - assert.deepEqual(await getLinesAsArray(3), ['', ' f', 'ghi']); + await pollFor(page, () => getLinesAsArray(3), ['', ' f', 'ghi']); await page.evaluate(` // 2: Erase Saved Lines (scrollback) window.term.reset() window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[3J') `); - assert.equal(await page.evaluate(`window.term.buffer.length`), 5); - assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); + await pollFor(page, () => page.evaluate(`window.term.buffer.length`), 5); + await pollFor(page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); }); it('DECSED: Erase in Display, VT220 - CSI ? Ps J', async function(): Promise { @@ -174,26 +172,26 @@ describe('InputHandler Integration Tests', function(): void { window.term.resize(5, 5); window.term.write('${fixture}\x1b[?J') `); - assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']); + await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']); await page.evaluate(` // 0: Erase Below window.term.reset() window.term.write('${fixture}\x1b[?0J') `); - assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']); + await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']); await page.evaluate(` // 1: Erase Above window.term.reset() window.term.write('${fixture}\x1b[?1J') `); - assert.deepEqual(await getLinesAsArray(3), ['', ' f', 'ghi']); + await pollFor(page, () => getLinesAsArray(3), ['', ' f', 'ghi']); await page.evaluate(` // 2: Erase Saved Lines (scrollback) window.term.reset() window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[?3J') `); - assert.equal(await page.evaluate(`window.term.buffer.length`), 5); - assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); + await pollFor(page, () => page.evaluate(`window.term.buffer.length`), 5); + await pollFor(page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); }); it('IL: Insert Ps Line(s) (default = 1) - CSI Ps L', async function(): Promise { @@ -203,7 +201,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[2Lb') `); - assert.deepEqual(await getLinesAsArray(4), ['b', '', 'a', 'foo']); + await pollFor(page, () => getLinesAsArray(4), ['b', '', 'a', 'foo']); }); it('DL: Delete Ps Line(s) (default = 1) - CSI Ps M', async function(): Promise { @@ -213,7 +211,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\x1b[1Ed\\ne\\nf\x1b[2F\x1b[2M') `); - assert.deepEqual(await getLinesAsArray(5), [' b', ' f', '', '', '']); + await pollFor(page, () => getLinesAsArray(5), [' b', ' f', '', '', '']); }); it('DCH: Delete Ps Character(s) (default = 1) - CSI Ps P', async function(): Promise { @@ -223,7 +221,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\\n\\rdef\x1b[2;1H\x1b[2P') `); - assert.deepEqual(await getLinesAsArray(2), ['bc', 'f']); + await pollFor(page, () => getLinesAsArray(2), ['bc', 'f']); }); describe('DSR: Device Status Report', () => { @@ -232,31 +230,31 @@ describe('InputHandler Integration Tests', function(): void { window.term.onData(e => window.result = e); window.term.write('\\x1b[5n'); `); - assert.equal(await page.evaluate(`window.result`), '\x1b[0n'); + await pollFor(page, () => page.evaluate(`window.result`), '\x1b[0n'); }); it('Report Cursor Position (CPR) - CSI 6 n', async function(): Promise { await page.evaluate(`window.term.write('\\n\\nfoo')`); - assert.deepEqual(await page.evaluate(` + await pollFor(page, () => page.evaluate(` [window.term.buffer.cursorY, window.term.buffer.cursorX] `), [2, 3]); await page.evaluate(` window.term.onData(e => window.result = e); window.term.write('\\x1b[6n'); `); - assert.equal(await page.evaluate(`window.result`), '\x1b[3;4R'); + await pollFor(page, () => page.evaluate(`window.result`), '\x1b[3;4R'); }); it('Report Cursor Position (DECXCPR) - CSI ? 6 n', async function(): Promise { await page.evaluate(`window.term.write('\\n\\nfoo')`); - assert.deepEqual(await page.evaluate(` + await pollFor(page, () => page.evaluate(` [window.term.buffer.cursorY, window.term.buffer.cursorX] `), [2, 3]); await page.evaluate(` window.term.onData(e => window.result = e); window.term.write('\\x1b[?6n'); `); - assert.equal(await page.evaluate(`window.result`), '\x1b[?3;4R'); + await pollFor(page, () => page.evaluate(`window.result`), '\x1b[?3;4R'); }); }); @@ -277,22 +275,23 @@ describe('InputHandler Integration Tests', function(): void { await page.mouse.up(); // Clear selection await page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2); - assert.equal(await page.evaluate(`window.term.getSelection().length`), 0); + await pollFor(page, () => page.evaluate(`window.term.getSelection().length`), 0); // Enable mouse events await page.evaluate(`window.term.write('\x1b[?1003h')`); // Click and drag and ensure there is no selection await page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2); await page.mouse.down(); await page.mouse.move((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 4); - assert.equal(await page.evaluate(`window.term.getSelection().length`), 0, 'mouse events are on so there should be no selection'); + // mouse events are on so there should be no selection + await pollFor(page, () => page.evaluate(`window.term.getSelection().length`), 0); await page.mouse.up(); }); it('Pm = 2004, Set bracketed paste mode', async function(): Promise { - assert.equal(await simulatePaste('foo'), 'foo'); + await pollFor(page, () => simulatePaste('foo'), 'foo'); await page.evaluate(`window.term.write('\x1b[?2004h')`); - assert.equal(await simulatePaste('bar'), '\x1b[200~bar\x1b[201~'); + await pollFor(page, () => simulatePaste('bar'), '\x1b[200~bar\x1b[201~'); await page.evaluate(`window.term.write('\x1b[?2004l')`); - assert.equal(await simulatePaste('baz'), 'baz'); + await pollFor(page, () => simulatePaste('baz'), 'baz'); }); }); }); @@ -309,32 +308,32 @@ describe('InputHandler Integration Tests', function(): void { window.term.writeln(''); window.term.write('#\x1b[5b'); `); - assert.deepEqual(await getLinesAsArray(4), ['##', '##', '##', '######']); - assert.deepEqual(await getCursor(), {col: 6, row: 3}); + await pollFor(page, () => getLinesAsArray(4), ['##', '##', '##', '######']); + await pollFor(page, () => getCursor(), {col: 6, row: 3}); // should not repeat on fullwidth chars await page.evaluate(` window.term.reset(); window.term.write('¥\x1b[10b'); `); - assert.deepEqual(await getLinesAsArray(1), ['¥']); + await pollFor(page, () => getLinesAsArray(1), ['¥']); // should repeat only base char of combining await page.evaluate(` window.term.reset(); window.term.write('e\u0301\x1b[5b'); `); - assert.deepEqual(await getLinesAsArray(1), ['e\u0301eeeee']); + await pollFor(page, () => getLinesAsArray(1), ['e\u0301eeeee']); // should wrap correctly await page.evaluate(` window.term.reset(); window.term.write('#\x1b[15b'); `); - assert.deepEqual(await getLinesAsArray(2), ['##########', '######']); + await pollFor(page, () => getLinesAsArray(2), ['##########', '######']); await page.evaluate(` window.term.reset(); window.term.write('\x1b[?7l'); // disable wrap around window.term.write('#\x1b[15b'); `); - assert.deepEqual(await getLinesAsArray(2), ['##########', '']); + await pollFor(page, () => getLinesAsArray(2), ['##########', '']); // any successful sequence should reset REP await page.evaluate(` window.term.reset(); @@ -344,7 +343,7 @@ describe('InputHandler Integration Tests', function(): void { window.term.writeln(''); window.term.write('abcdefg\x1b[3D\x1b[10b#\x1b[3b'); `); - assert.deepEqual(await getLinesAsArray(3), ['#', ' #', 'abcd####']); + await pollFor(page, () => getLinesAsArray(3), ['#', ' #', 'abcd####']); }); }); @@ -360,7 +359,7 @@ describe('InputHandler Integration Tests', function(): void { window.term.resize(10, 4); window.term.write('\\x1b[?47l\\x1b8'); `); - assert.deepEqual(await getCursor(), {col: 1, row: 3}); + await pollFor(page, () => getCursor(), {col: 1, row: 3}); }); }); }); diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index 90c18f753..7cae660f5 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -4,15 +4,17 @@ */ import * as puppeteer from 'puppeteer'; -import { assert } from 'chai'; import { ITerminalOptions } from 'xterm'; +import { pollFor, writeSync } from './TestUtils'; const APP = 'http://127.0.0.1:3000/test'; let browser: puppeteer.Browser; let page: puppeteer.Page; -const width = 1024; -const height = 768; +// adjusted to work inside devcontainer +// see https://github.com/xtermjs/xterm.js/issues/2379 +const width = 1280; +const height = 960; // adjust terminal row/col size so we can test // >80 up to 223 and >255 @@ -165,7 +167,6 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number; let col: number; // unpack msg const report = String.fromCharCode.apply(null, msg); - // console.log([report]); // skip non mouse reports if (!report || report[0] !== '\x1b') { return report; @@ -177,14 +178,6 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number; col: report.charCodeAt(4) - 32, row: report.charCodeAt(5) - 32 }; - case 'UTF8': - // TODO: once the binary patch is in place, - // use UTF8 byte check here - return { - state: evalButtonCode(report.charCodeAt(3) - 32), - col: report.charCodeAt(4) - 32, - row: report.charCodeAt(5) - 32 - }; case 'SGR': sReport = report.slice(3, -1); [buttonCode, col, row] = sReport.split(';').map(el => parseInt(el)); @@ -193,10 +186,6 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number; state.action = 'release'; } return {state, row, col}; - case 'URXVT': - sReport = report.slice(2, -1); - [buttonCode, col, row] = sReport.split(';').map(el => parseInt(el)); - return {state: evalButtonCode(buttonCode - 32), row, col}; default: return { state: evalButtonCode(report.charCodeAt(3) - 32), @@ -209,22 +198,17 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number; /** * Mouse tracking tests. */ -describe('Mouse Tracking Tests', function(): void { - this.timeout(60000); - - before(async function(): Promise { +describe('Mouse Tracking Tests', () => { + before(async function(): Promise { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); }); - after(() => { - browser.close(); - }); + after(async () => browser.close()); beforeEach(async () => { await page.goto(APP); @@ -236,6 +220,7 @@ describe('Mouse Tracking Tests', function(): void { await page.evaluate(` window.calls = []; window.term.onData(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); + window.term.onBinary(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); window.term.setOption('fontSize', ${fontSize}); window.term.resize(${cols}, ${rows}); `); @@ -253,30 +238,35 @@ describe('Mouse Tracking Tests', function(): void { const encoding = 'DEFAULT'; await resetMouseModes(); await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?9h');`); + await writeSync(page, '\x1b[?9h'); // test at 0,0 await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // mouseup should not report await mouseUp('left'); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); // mousemove should not report await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // higher than 223 should not report at all + await mouseMove(257, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + await pollFor(page, () => getReports(encoding), []); // button press/move/release tests // left button @@ -285,7 +275,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // middle button // bug: default action not cancelled (adds data to getReports from clipboard under X11) // await mouseMove(43, 24); @@ -293,7 +283,7 @@ describe('Mouse Tracking Tests', function(): void { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -301,15 +291,15 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); // wheel await mouseMove(43, 24); await getReports(encoding); // clear reports await wheelUp(); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); await wheelDown(); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); // modifiers // CTRL @@ -321,7 +311,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // ALT @@ -333,7 +323,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // SHIFT // note: caught by selection manager @@ -348,9 +338,9 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Shift'); if (noShift) { - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); } else { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); } @@ -369,152 +359,34 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Control'); await page.keyboard.up('Alt'); // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - }); - it.skip('UTF8 encoding', async () => { - const encoding = 'UTF8'; - await resetMouseModes(); - await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1005h');`); - - // test at 0,0 - await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - // mouseup should not report - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), []); - - // mousemove should not report - await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - // test at max rows/cols - await mouseMove(cols - 1, rows - 1); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - // button press/move/release tests - // left button - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - // middle button - // bug: default action not cancelled (adds data to getReports from clipboard under X11) - // await mouseMove(43, 24); - // await getReports(encoding); // clear reports - // await mouseDown('middle'); - // await mouseMove(44, 24); - // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); - // right button - // bug: default action not cancelled (popup shown) - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('right'); - await mouseMove(44, 24); - await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); - - // wheel - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await wheelUp(); - assert.deepEqual(await getReports(encoding), []); - await wheelDown(); - assert.deepEqual(await getReports(encoding), []); - - // modifiers - // CTRL - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - - // ALT - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - // SHIFT - // note: caught by selection manager - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Shift'); // defaults to ShiftLeft - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Shift'); - if (noShift) { - assert.deepEqual(await getReports(encoding), []); - } else { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} - ]); - } - - // all modifiers - // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again? - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); }); it('SGR encoding', async () => { const encoding = 'SGR'; await resetMouseModes(); await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1006h');`); + await writeSync(page, '\x1b[?9h\x1b[?1006h'); // test at 0,0 await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // mouseup should not report await mouseUp('left'); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); // mousemove should not report await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // test at max rows/cols await mouseMove(cols - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // button press/move/release tests // left button @@ -523,7 +395,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // middle button // bug: default action not cancelled (adds data to getReports from clipboard under X11) // await mouseMove(43, 24); @@ -531,7 +403,7 @@ describe('Mouse Tracking Tests', function(): void { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -539,15 +411,15 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); // wheel await mouseMove(43, 24); await getReports(encoding); // clear reports await wheelUp(); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); await wheelDown(); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); // modifiers // CTRL @@ -559,7 +431,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // ALT @@ -571,7 +443,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // SHIFT // note: caught by selection manager @@ -584,9 +456,9 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Shift'); if (noShift) { - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); } else { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); } @@ -605,126 +477,7 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Control'); await page.keyboard.up('Alt'); // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - }); - it.skip('URXVT encoding', async () => { - // bug: always reports +1 for row/col (temp. fixed in parseReport to pass tests) - const encoding = 'URXVT'; - await resetMouseModes(); - await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1015h');`); - - // test at 0,0 - await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - // mouseup should not report - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), []); - - // mousemove should not report - await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - // test at max rows/cols - await mouseMove(cols - 1, rows - 1); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - // button press/move/release tests - // left button - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - // middle button - // bug: default action not cancelled (adds data to getReports from clipboard under X11) - // await mouseMove(43, 24); - // await getReports(encoding); // clear reports - // await mouseDown('middle'); - // await mouseMove(44, 24); - // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); - // right button - // bug: default action not cancelled (popup shown) - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('right'); - await mouseMove(44, 24); - await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); - - // wheel - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await wheelUp(); - assert.deepEqual(await getReports(encoding), []); - await wheelDown(); - assert.deepEqual(await getReports(encoding), []); - - // modifiers - // CTRL - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - - // ALT - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - - // SHIFT - // note: caught by selection manager - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Shift'); // defaults to ShiftLeft - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Shift'); - if (noShift) { - assert.deepEqual(await getReports(encoding), []); - } else { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} - ]); - } - - // all modifiers - // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again? - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); }); }); describe('DECSET 1000 (VT200 mouse)', () => { @@ -739,39 +492,38 @@ describe('Mouse Tracking Tests', function(): void { const encoding = 'DEFAULT'; await resetMouseModes(); await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1000h');`); + await writeSync(page, '\x1b[?1000h'); // test at 0,0 await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // mousemove should not report await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + await pollFor(page, () => getReports(encoding), [ + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests @@ -781,7 +533,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); @@ -792,7 +544,7 @@ describe('Mouse Tracking Tests', function(): void { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -800,7 +552,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); @@ -809,9 +561,9 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(43, 24); await getReports(encoding); // clear reports await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); // modifiers // CTRL @@ -823,7 +575,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} @@ -838,7 +590,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} @@ -856,11 +608,11 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Shift'); if (noShift) { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} @@ -881,156 +633,7 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Control'); await page.keyboard.up('Alt'); // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} - ]); - }); - it.skip('UTF8 encoding', async () => { - const encoding = 'UTF8'; - await resetMouseModes(); - await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1005h');`); - - // test at 0,0 - await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mouseup should report, encoding cannot report released button - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mousemove should not report - await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // test at max rows/cols - await mouseMove(cols - 1, rows - 1); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // button press/move/release tests - // left button - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - // middle button - // bug: default action not cancelled (adds data to getReports from clipboard under X11) - // await mouseMove(43, 24); - // await getReports(encoding); // clear reports - // await mouseDown('middle'); - // await mouseMove(44, 24); - // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); - // right button - // bug: default action not cancelled (popup shown) - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('right'); - await mouseMove(44, 24); - await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // wheel - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - - // modifiers - // CTRL - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} - ]); - - // ALT - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} - ]); - - // SHIFT - // note: press/release caught by selection manager - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Shift'); // defaults to ShiftLeft - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Shift'); - if (noShift) { - assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } else { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } - - // all modifiers - // bug: Shift not working - selection manager? - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} @@ -1040,26 +643,26 @@ describe('Mouse Tracking Tests', function(): void { const encoding = 'SGR'; await resetMouseModes(); await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1006h');`); + await writeSync(page, '\x1b[?1000h\x1b[?1006h'); // test at 0,0 await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mousemove should not report await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 51, row: 11, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); @@ -1068,7 +671,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(cols - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: cols, row: rows, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); @@ -1080,7 +683,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); @@ -1091,7 +694,7 @@ describe('Mouse Tracking Tests', function(): void { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -1099,7 +702,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: 'right', modifier: {control: false, shift: false, meta: false}}} ]); @@ -1108,9 +711,9 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(43, 24); await getReports(encoding); // clear reports await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); // modifiers // CTRL @@ -1122,7 +725,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} @@ -1137,7 +740,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} @@ -1154,11 +757,11 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Shift'); if (noShift) { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} @@ -1179,161 +782,12 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Control'); await page.keyboard.up('Alt'); // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); }); - it.skip('URXVT encoding', async () => { - const encoding = 'URXVT'; - await resetMouseModes(); - await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1015h');`); - - // test at 0,0 - await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mouseup should report, encoding cannot report released button - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mousemove should not report - await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // test at max rows/cols - await mouseMove(cols - 1, rows - 1); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // button press/move/release tests - // left button - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - // middle button - // bug: default action not cancelled (adds data to getReports from clipboard under X11) - // await mouseMove(43, 24); - // await getReports(encoding); // clear reports - // await mouseDown('middle'); - // await mouseMove(44, 24); - // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); - // right button - // bug: default action not cancelled (popup shown) - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('right'); - await mouseMove(44, 24); - await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // wheel - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - - // modifiers - // CTRL - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} - ]); - - // ALT - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} - ]); - - // SHIFT - // note: press/release caught by selection manager - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Shift'); // defaults to ShiftLeft - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Shift'); - if (noShift) { - assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } else { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } - - // all modifiers - // bug: Shift not working - selection manager? - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} - ]); - }); }); describe('DECSET 1002 (xterm with drag)', () => { /** @@ -1347,39 +801,38 @@ describe('Mouse Tracking Tests', function(): void { const encoding = 'DEFAULT'; await resetMouseModes(); await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1002h');`); + await writeSync(page, '\x1b[?1002h'); // test at 0,0 await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // mousemove should not report await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + await pollFor(page, () => getReports(encoding), [ + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests @@ -1389,7 +842,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} @@ -1401,7 +854,7 @@ describe('Mouse Tracking Tests', function(): void { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -1409,7 +862,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} @@ -1419,9 +872,9 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(43, 24); await getReports(encoding); // clear reports await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); // modifiers // CTRL @@ -1433,7 +886,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, @@ -1449,7 +902,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, @@ -1467,11 +920,11 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Shift'); if (noShift) { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, @@ -1493,165 +946,7 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Control'); await page.keyboard.up('Alt'); // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} - ]); - }); - it.skip('UTF8 encoding', async () => { - const encoding = 'UTF8'; - await resetMouseModes(); - await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1002h\x1b[?1005h');`); - - // test at 0,0 - // bug: release is fired immediately - await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mouseup should report, encoding cannot report released button - // bug: release already fired thus no event here - expected: release event - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mousemove should not report - await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // test at max rows/cols - await mouseMove(cols - 1, rows - 1); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // button press/move/release tests - // left button - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - // middle button - // bug: default action not cancelled (adds data to getReports from clipboard under X11) - // await mouseMove(43, 24); - // await getReports(encoding); // clear reports - // await mouseDown('middle'); - // await mouseMove(44, 24); - // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); - // right button - // bug: default action not cancelled (popup shown) - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('right'); - await mouseMove(44, 24); - await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // wheel - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - - // modifiers - // CTRL - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} - ]); - - // ALT - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} - ]); - - // SHIFT - // note: press/release/drag caught by selection manager - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Shift'); // defaults to ShiftLeft - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Shift'); - if (noShift) { - assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } else { - // bug: completely messed up - wrong modifier, only partially reported - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } - - // all modifiers - // bug: Shift not working - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, @@ -1662,27 +957,27 @@ describe('Mouse Tracking Tests', function(): void { const encoding = 'SGR'; await resetMouseModes(); await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1002h\x1b[?1006h');`); + await writeSync(page, '\x1b[?1002h\x1b[?1006h'); // test at 0,0 // bug: release is fired immediately await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mousemove should not report await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); + await pollFor(page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 51, row: 11, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); @@ -1691,7 +986,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(cols - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: cols, row: rows, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); @@ -1703,7 +998,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} @@ -1715,7 +1010,7 @@ describe('Mouse Tracking Tests', function(): void { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -1723,7 +1018,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: 'right', modifier: {control: false, shift: false, meta: false}}} @@ -1733,9 +1028,9 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(43, 24); await getReports(encoding); // clear reports await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); // modifiers // CTRL @@ -1747,7 +1042,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: false}}}, @@ -1763,7 +1058,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: true}}}, @@ -1781,11 +1076,11 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Shift'); if (noShift) { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: true, meta: false}}}, @@ -1807,169 +1102,13 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Control'); await page.keyboard.up('Alt'); // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); }); - it.skip('URXVT encoding', async () => { - const encoding = 'URXVT'; - await resetMouseModes(); - await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1002h\x1b[?1015h');`); - - // test at 0,0 - // bug: release is fired immediately - await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mouseup should report, encoding cannot report released button - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mousemove should not report - await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), []); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // test at max rows/cols - await mouseMove(cols - 1, rows - 1); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // button press/move/release tests - // left button - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - // middle button - // bug: default action not cancelled (adds data to getReports from clipboard under X11) - // await mouseMove(43, 24); - // await getReports(encoding); // clear reports - // await mouseDown('middle'); - // await mouseMove(44, 24); - // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); - // right button - // bug: default action not cancelled (popup shown) - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await mouseDown('right'); - await mouseMove(44, 24); - await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // wheel - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - - // modifiers - // CTRL - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} - ]); - - // ALT - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} - ]); - - // SHIFT - // note: press/release/drag caught by selection manager - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Shift'); // defaults to ShiftLeft - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Shift'); - if (noShift) { - assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } else { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } - - // all modifiers - // bug: Shift not working - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} - ]); - }); }); describe('DECSET 1003 (xterm any event)', () => { /** @@ -1980,42 +1119,41 @@ describe('Mouse Tracking Tests', function(): void { const encoding = 'DEFAULT'; await resetMouseModes(); await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1003h');`); + await writeSync(page, '\x1b[?1003h'); // test at 0,0 await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // mousemove should report await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 51, row: 11, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}} ]); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 95, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + await pollFor(page, () => getReports(encoding), [ + {col: 223, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests @@ -2024,7 +1162,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, @@ -2037,14 +1175,14 @@ describe('Mouse Tracking Tests', function(): void { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, @@ -2055,9 +1193,9 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(43, 24); await getReports(encoding); // clear reports await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); // modifiers // CTRL @@ -2068,7 +1206,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: false}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, @@ -2084,7 +1222,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: true}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, @@ -2102,12 +1240,12 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Shift'); if (noShift) { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, @@ -2129,168 +1267,7 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Control'); await page.keyboard.up('Alt'); // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: true}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} - ]); - }); - it.skip('UTF8 encoding', async () => { - const encoding = 'UTF8'; - await resetMouseModes(); - await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1003h\x1b[?1005h');`); - - // test at 0,0 - await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mouseup should report, encoding cannot report released button - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mousemove should report - await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), [ - {col: 51, row: 11, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: cols, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // button press/move/release tests - // left button - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - // middle button - // bug: default action not cancelled (adds data to getReports from clipboard under X11) - // await mouseMove(43, 24); - // await getReports(encoding); // clear reports - // await mouseDown('middle'); - // await mouseMove(44, 24); - // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); - // right button - // bug: default action not cancelled (popup shown) - await mouseMove(43, 24); - await mouseDown('right'); - await mouseMove(44, 24); - await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // wheel - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - - // modifiers - // CTRL - await page.keyboard.down('Control'); - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} - ]); - - // ALT - await page.keyboard.down('Alt'); - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: true}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} - ]); - - // SHIFT - // note: press/release/drag caught by selection manager - await page.keyboard.down('Shift'); - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Shift'); - if (noShift) { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } else { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } - - // all modifiers - // bug: Shift not working - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: true}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, @@ -2302,28 +1279,28 @@ describe('Mouse Tracking Tests', function(): void { const encoding = 'SGR'; await resetMouseModes(); await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1003h\x1b[?1006h');`); + await writeSync(page, '\x1b[?1003h\x1b[?1006h'); // test at 0,0 await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mousemove should report await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 51, row: 11, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}} ]); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 51, row: 11, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); @@ -2334,7 +1311,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(cols - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: cols, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: cols, row: rows, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} @@ -2346,7 +1323,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, @@ -2359,14 +1336,14 @@ describe('Mouse Tracking Tests', function(): void { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, @@ -2377,9 +1354,9 @@ describe('Mouse Tracking Tests', function(): void { await mouseMove(43, 24); await getReports(encoding); // clear reports await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); // modifiers // CTRL @@ -2390,7 +1367,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: false}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, @@ -2406,7 +1383,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); await wheelDown(); await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: true}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, @@ -2424,12 +1401,12 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Shift'); if (noShift) { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, @@ -2451,7 +1428,7 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Control'); await page.keyboard.up('Alt'); // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ + await pollFor(page, () => getReports(encoding), [ {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: true}}}, {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, @@ -2459,167 +1436,6 @@ describe('Mouse Tracking Tests', function(): void { {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); }); - it.skip('URXVT encoding', async () => { - const encoding = 'URXVT'; - await resetMouseModes(); - await mouseMove(0, 0); - await page.evaluate(`window.term.write('\x1b[?1003h\x1b[?1015h');`); - - // test at 0,0 - await mouseDown('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mouseup should report, encoding cannot report released button - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // mousemove should report - await mouseMove(50, 10); - assert.deepEqual(await getReports(encoding), [ - {col: 51, row: 11, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); - await mouseDown('left'); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: cols, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // button press/move/release tests - // left button - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - // middle button - // bug: default action not cancelled (adds data to getReports from clipboard under X11) - // await mouseMove(43, 24); - // await getReports(encoding); // clear reports - // await mouseDown('middle'); - // await mouseMove(44, 24); - // await mouseUp('middle'); - // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); - // right button - // bug: default action not cancelled (popup shown) - await mouseMove(43, 24); - await mouseDown('right'); - await mouseMove(44, 24); - await mouseUp('right'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} - ]); - - // wheel - await mouseMove(43, 24); - await getReports(encoding); // clear reports - await wheelUp(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - await wheelDown(); - assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); - - // modifiers - // CTRL - await page.keyboard.down('Control'); - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} - ]); - - // ALT - await page.keyboard.down('Alt'); - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Alt'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: true}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} - ]); - - // SHIFT - // note: press/release/drag caught by selection manager - await page.keyboard.down('Shift'); - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Shift'); - if (noShift) { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } else { - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} - ]); - } - - // all modifiers - // bug: Shift not working - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); - await mouseMove(43, 24); - await mouseDown('left'); - await mouseMove(44, 24); - await mouseUp('left'); - await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: true}}}, - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} - ]); - }); }); /** * move tests with multiple buttons pressed: diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts index 28f7d8b34..72f567994 100644 --- a/test/api/Parser.api.ts +++ b/test/api/Parser.api.ts @@ -6,6 +6,7 @@ import * as puppeteer from 'puppeteer'; import { assert } from 'chai'; import { ITerminalOptions } from 'xterm'; +import { writeSync } from './TestUtils'; const APP = 'http://127.0.0.1:3000/test'; @@ -15,13 +16,10 @@ const width = 800; const height = 600; describe('Parser Integration Tests', function(): void { - this.timeout(20000); - before(async function(): Promise { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); @@ -29,9 +27,7 @@ describe('Parser Integration Tests', function(): void { await openTerminal(); }); - after(() => { - browser.close(); - }); + after(async () => browser.close()); describe('addCsiHandler', () => { it('should call custom CSI handler with js array params', async () => { @@ -43,9 +39,7 @@ describe('Parser Integration Tests', function(): void { return false; }, ''); `); - await page.evaluate(` - window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); - `); + await writeSync(page, '\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); }); }); @@ -67,9 +61,7 @@ describe('Parser Integration Tests', function(): void { return false; }); `); - await page.evaluate(` - window.term.write('\x1bP1;2+psome data\x1b\\\\'); - `); + await writeSync(page, '\x1bP1;2+psome data\x1b\\\\'); assert.deepEqual(await page.evaluate(`(() => _customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); }); }); @@ -91,9 +83,7 @@ describe('Parser Integration Tests', function(): void { return false; }); `); - await page.evaluate(` - window.term.write('\x1b(B'); - `); + await writeSync(page, '\x1b(B'); assert.deepEqual(await page.evaluate(`(() => _customEscHandlerCallStack)();`), ['C', 'B']); }); }); @@ -115,9 +105,7 @@ describe('Parser Integration Tests', function(): void { return false; }); `); - await page.evaluate(` - window.term.write('\x1b]1234;some data\x07'); - `); + await writeSync(page, '\x1b]1234;some data\x07'); assert.deepEqual(await page.evaluate(`(() => _customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]); }); }); diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 034b04c49..40deecf20 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -6,6 +6,7 @@ import * as puppeteer from 'puppeteer'; import { assert } from 'chai'; import { ITerminalOptions } from 'xterm'; +import { pollFor, timeout, writeSync } from './TestUtils'; const APP = 'http://127.0.0.1:3000/test'; @@ -15,54 +16,46 @@ const width = 800; const height = 600; describe('API Integration Tests', function(): void { - this.timeout(20000); - - before(async function(): Promise { + before(async () => { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); }); - after(() => { - browser.close(); - }); + after(async () => browser.close()); + beforeEach(async () => page.goto(APP)); - beforeEach(async () => { - await page.goto(APP); - }); - - it('Default options', async function(): Promise { + it('Default options', async () => { await openTerminal(); assert.equal(await page.evaluate(`window.term.cols`), 80); assert.equal(await page.evaluate(`window.term.rows`), 24); }); - it('write', async function(): Promise { + it('write', async () => { await openTerminal(); await page.evaluate(` window.term.write('foo'); window.term.write('bar'); window.term.write('文'); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foobar文'); }); - it('write with callback', async function(): Promise { + it('write with callback', async () => { await openTerminal(); await page.evaluate(` window.term.write('foo', () => { window.__x = 'a'; }); window.term.write('bar', () => { window.__x += 'b'; }); window.term.write('文', () => { window.__x += 'c'; }); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); - assert.equal(await page.evaluate(`window.__x`), 'abc'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foobar文'); + await pollFor(page, `window.__x`, 'abc'); }); - it('write - bytes (UTF8)', async function(): Promise { + it('write - bytes (UTF8)', async () => { await openTerminal(); await page.evaluate(` // foo @@ -72,10 +65,10 @@ describe('API Integration Tests', function(): void { // 文 window.term.write(new Uint8Array([230, 150, 135])); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foobar文'); }); - it('write - bytes (UTF8) with callback', async function(): Promise { + it('write - bytes (UTF8) with callback', async () => { await openTerminal(); await page.evaluate(` // foo @@ -85,48 +78,48 @@ describe('API Integration Tests', function(): void { // 文 window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; }); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); - assert.equal(await page.evaluate(`window.__x`), 'ABC'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foobar文'); + await pollFor(page, `window.__x`, 'ABC'); }); - it('writeln', async function(): Promise { + it('writeln', async () => { await openTerminal(); await page.evaluate(` window.term.writeln('foo'); window.term.writeln('bar'); window.term.writeln('文'); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo'); + await pollFor(page, `window.term.buffer.getLine(1).translateToString(true)`, 'bar'); + await pollFor(page, `window.term.buffer.getLine(2).translateToString(true)`, '文'); }); - it('writeln with callback', async function(): Promise { + it('writeln with callback', async () => { await openTerminal(); await page.evaluate(` window.term.writeln('foo', () => { window.__x = '1'; }); window.term.writeln('bar', () => { window.__x += '2'; }); window.term.writeln('文', () => { window.__x += '3'; }); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文'); - assert.equal(await page.evaluate(`window.__x`), '123'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo'); + await pollFor(page, `window.term.buffer.getLine(1).translateToString(true)`, 'bar'); + await pollFor(page, `window.term.buffer.getLine(2).translateToString(true)`, '文'); + await pollFor(page, `window.__x`, '123'); }); - it('writeln - bytes (UTF8)', async function(): Promise { + it('writeln - bytes (UTF8)', async () => { await openTerminal(); await page.evaluate(` window.term.writeln(new Uint8Array([102, 111, 111])); window.term.writeln(new Uint8Array([98, 97, 114])); window.term.writeln(new Uint8Array([230, 150, 135])); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文'); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo'); + await pollFor(page, `window.term.buffer.getLine(1).translateToString(true)`, 'bar'); + await pollFor(page, `window.term.buffer.getLine(2).translateToString(true)`, '文'); }); - it('paste', async function(): Promise { + it('paste', async () => { await openTerminal(); await page.evaluate(` window.calls = []; @@ -137,26 +130,28 @@ describe('API Integration Tests', function(): void { window.term.paste('foo'); }); `); - assert.deepEqual(await page.evaluate(`window.calls`), ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~']); + await pollFor(page, `window.calls`, ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~']); }); - it('clear', async function(): Promise { + it('clear', async () => { await openTerminal({ rows: 5 }); await page.evaluate(` window.term.write('test0'); + window.parsed = 0; for (let i = 1; i < 10; i++) { - window.term.write('\\n\\rtest' + i); + window.term.write('\\n\\rtest' + i, () => window.parsed++); } `); + await pollFor(page, `window.parsed`, 9); await page.evaluate(`window.term.clear()`); - assert.equal(await page.evaluate(`window.term.buffer.length`), '5'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'test9'); + await pollFor(page, `window.term.buffer.length`, 5); + await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'test9'); for (let i = 1; i < 5; i++) { - assert.equal(await page.evaluate(`window.term.buffer.getLine(${i}).translateToString(true)`), ''); + await pollFor(page, `window.term.buffer.getLine(${i}).translateToString(true)`, ''); } }); - it('getOption, setOption', async function(): Promise { + it('getOption, setOption', async () => { await openTerminal(); assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'canvas'); await page.evaluate(`window.term.setOption('rendererType', 'dom')`); @@ -164,9 +159,10 @@ describe('API Integration Tests', function(): void { }); describe('renderer', () => { - it('foreground', async function(): Promise { + it('foreground', async () => { await openTerminal({ rendererType: 'dom' }); - await page.evaluate(`window.term.write('\\x1b[30m0\\x1b[31m1\\x1b[32m2\\x1b[33m3\\x1b[34m4\\x1b[35m5\\x1b[36m6\\x1b[37m7')`); + await writeSync(page, '\\x1b[30m0\\x1b[31m1\\x1b[32m2\\x1b[33m3\\x1b[34m4\\x1b[35m5\\x1b[36m6\\x1b[37m7'); + await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); assert.deepEqual(await page.evaluate(` [ document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, @@ -188,9 +184,10 @@ describe('API Integration Tests', function(): void { ]); }); - it('background', async function(): Promise { + it('background', async () => { await openTerminal({ rendererType: 'dom' }); - await page.evaluate(`window.term.write('\\x1b[40m0\\x1b[41m1\\x1b[42m2\\x1b[43m3\\x1b[44m4\\x1b[45m5\\x1b[46m6\\x1b[47m7')`); + await writeSync(page, '\\x1b[40m0\\x1b[41m1\\x1b[42m2\\x1b[43m3\\x1b[44m4\\x1b[45m5\\x1b[46m6\\x1b[47m7'); + await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); assert.deepEqual(await page.evaluate(` [ document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, @@ -213,7 +210,7 @@ describe('API Integration Tests', function(): void { }); }); - it('selection', async function(): Promise { + it('selection', async () => { await openTerminal({ rows: 5, cols: 5 }); await page.evaluate(`window.term.write('\\n\\nfoo\\n\\n\\rbar\\n\\n\\rbaz')`); assert.equal(await page.evaluate(`window.term.hasSelection()`), false); @@ -233,7 +230,7 @@ describe('API Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { startColumn: 1, startRow: 2, endColumn: 3, endRow: 2 }); }); - it('focus, blur', async function(): Promise { + it('focus, blur', async () => { await openTerminal(); assert.equal(await page.evaluate(`document.activeElement.className`), ''); await page.evaluate(`window.term.focus()`); @@ -243,7 +240,7 @@ describe('API Integration Tests', function(): void { }); describe('loadAddon', () => { - it('constructor', async function(): Promise { + it('constructor', async () => { await openTerminal({ cols: 5 }); await page.evaluate(` window.cols = 0; @@ -255,7 +252,7 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.cols`), 5); }); - it('dispose (addon)', async function(): Promise { + it('dispose (addon)', async () => { await openTerminal(); await page.evaluate(` window.disposeCalled = false @@ -270,7 +267,7 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.disposeCalled`), true); }); - it('dispose (terminal)', async function(): Promise { + it('dispose (terminal)', async () => { await openTerminal(); await page.evaluate(` window.disposeCalled = false @@ -286,19 +283,19 @@ describe('API Integration Tests', function(): void { }); describe('Events', () => { - it('onCursorMove', async function(): Promise { + it('onCursorMove', async () => { await openTerminal(); await page.evaluate(` window.callCount = 0; window.term.onCursorMove(e => window.callCount++); window.term.write('foo'); `); - assert.equal(await page.evaluate(`window.callCount`), 1); + await pollFor(page, `window.callCount`, 1); await page.evaluate(`window.term.write('bar')`); - assert.equal(await page.evaluate(`window.callCount`), 2); + await pollFor(page, `window.callCount`, 2); }); - it('onData', async function(): Promise { + it('onData', async () => { await openTerminal(); await page.evaluate(` window.calls = []; @@ -308,7 +305,7 @@ describe('API Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`window.calls`), ['f', 'o', 'o']); }); - it('onKey', async function(): Promise { + it('onKey', async () => { await openTerminal(); await page.evaluate(` window.calls = []; @@ -318,19 +315,19 @@ describe('API Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`window.calls`), ['f', 'o', 'o']); }); - it('onLineFeed', async function(): Promise { + it('onLineFeed', async () => { await openTerminal(); await page.evaluate(` window.callCount = 0; window.term.onLineFeed(() => callCount++); window.term.writeln('foo'); `); - assert.equal(await page.evaluate(`window.callCount`), 1); + await pollFor(page, `window.callCount`, 1); await page.evaluate(`window.term.writeln('bar')`); - assert.equal(await page.evaluate(`window.callCount`), 2); + await pollFor(page, `window.callCount`, 2); }); - it('onScroll', async function(): Promise { + it('onScroll', async () => { await openTerminal({ rows: 5 }); await page.evaluate(` window.calls = []; @@ -339,94 +336,97 @@ describe('API Integration Tests', function(): void { window.term.writeln('foo'); } `); - assert.deepEqual(await page.evaluate(`window.calls`), []); + await pollFor(page, `window.calls`, []); await page.evaluate(`window.term.writeln('bar')`); - assert.deepEqual(await page.evaluate(`window.calls`), [1]); + await pollFor(page, `window.calls`, [1]); await page.evaluate(`window.term.writeln('baz')`); - assert.deepEqual(await page.evaluate(`window.calls`), [1, 2]); + await pollFor(page, `window.calls`, [1, 2]); }); - it('onSelectionChange', async function(): Promise { + it('onSelectionChange', async () => { await openTerminal(); await page.evaluate(` window.callCount = 0; window.term.onSelectionChange(() => window.callCount++); `); - assert.equal(await page.evaluate(`window.callCount`), 0); + await pollFor(page, `window.callCount`, 0); await page.evaluate(`window.term.selectAll()`); - assert.equal(await page.evaluate(`window.callCount`), 1); + await pollFor(page, `window.callCount`, 1); await page.evaluate(`window.term.clearSelection()`); - assert.equal(await page.evaluate(`window.callCount`), 2); + await pollFor(page, `window.callCount`, 2); }); - it('onRender', async function(): Promise { + it('onRender', async function (): Promise { + this.retries(3); await openTerminal(); + await timeout(20); // Ensure all init events are fired await page.evaluate(` window.calls = []; window.term.onRender(e => window.calls.push([e.start, e.end])); `); - assert.deepEqual(await page.evaluate(`window.calls`), []); + await pollFor(page, `window.calls`, []); await page.evaluate(`window.term.write('foo')`); - assert.deepEqual(await page.evaluate(`window.calls`), [[0, 0]]); + await pollFor(page, `window.calls`, [[0, 0]]); await page.evaluate(`window.term.write('bar\\n\\nbaz')`); - assert.deepEqual(await page.evaluate(`window.calls`), [[0, 0], [0, 2]]); + await pollFor(page, `window.calls`, [[0, 0], [0, 2]]); }); - it('onResize', async function(): Promise { + it('onResize', async () => { await openTerminal(); + await timeout(20); // Ensure all init events are fired await page.evaluate(` window.calls = []; window.term.onResize(e => window.calls.push([e.cols, e.rows])); `); - assert.deepEqual(await page.evaluate(`window.calls`), []); + await pollFor(page, `window.calls`, []); await page.evaluate(`window.term.resize(10, 5)`); - assert.deepEqual(await page.evaluate(`window.calls`), [[10, 5]]); + await pollFor(page, `window.calls`, [[10, 5]]); await page.evaluate(`window.term.resize(20, 15)`); - assert.deepEqual(await page.evaluate(`window.calls`), [[10, 5], [20, 15]]); + await pollFor(page, `window.calls`, [[10, 5], [20, 15]]); }); - it('onTitleChange', async function(): Promise { + it('onTitleChange', async () => { await openTerminal(); await page.evaluate(` window.calls = []; window.term.onTitleChange(e => window.calls.push(e)); `); - assert.deepEqual(await page.evaluate(`window.calls`), []); + await pollFor(page, `window.calls`, []); await page.evaluate(`window.term.write('\\x1b]2;foo\\x9c')`); - assert.deepEqual(await page.evaluate(`window.calls`), ['foo']); + await pollFor(page, `window.calls`, ['foo']); }); }); describe('buffer', () => { - it('cursorX, cursorY', async function(): Promise { + it('cursorX, cursorY', async () => { await openTerminal({ rows: 5, cols: 5 }); assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0); assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 0); - await page.evaluate(`window.term.write('foo')`); + await writeSync(page, 'foo'); assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 3); assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 0); - await page.evaluate(`window.term.write('\\n')`); + await writeSync(page, '\\n'); assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 3); assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1); - await page.evaluate(`window.term.write('\\r')`); + await writeSync(page, '\\r'); assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0); assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1); - await page.evaluate(`window.term.write('abcde')`); + await writeSync(page, 'abcde'); assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 5); assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1); - await page.evaluate(`window.term.write('\\n\\r\\n\\n\\n\\n\\n')`); + await writeSync(page, '\\n\\r\\n\\n\\n\\n\\n'); assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0); assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 4); }); - it('viewportY', async function(): Promise { + it('viewportY', async () => { await openTerminal({ rows: 5 }); assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0); - await page.evaluate(`window.term.write('\\n\\n\\n\\n')`); + await writeSync(page, '\\n\\n\\n\\n'); assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0); - await page.evaluate(`window.term.write('\\n')`); + await writeSync(page, '\\n'); assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 1); - await page.evaluate(`window.term.write('\\n\\n\\n\\n')`); + await writeSync(page, '\\n\\n\\n\\n'); assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 5); await page.evaluate(`window.term.scrollLines(-1)`); assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 4); @@ -434,14 +434,14 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0); }); - it('baseY', async function(): Promise { + it('baseY', async () => { await openTerminal({ rows: 5 }); assert.equal(await page.evaluate(`window.term.buffer.baseY`), 0); - await page.evaluate(`window.term.write('\\n\\n\\n\\n')`); + await writeSync(page, '\\n\\n\\n\\n'); assert.equal(await page.evaluate(`window.term.buffer.baseY`), 0); - await page.evaluate(`window.term.write('\\n')`); + await writeSync(page, '\\n'); assert.equal(await page.evaluate(`window.term.buffer.baseY`), 1); - await page.evaluate(`window.term.write('\\n\\n\\n\\n')`); + await writeSync(page, '\\n\\n\\n\\n'); assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5); await page.evaluate(`window.term.scrollLines(-1)`); assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5); @@ -449,44 +449,44 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5); }); - it('length', async function(): Promise { + it('length', async () => { await openTerminal({ rows: 5 }); assert.equal(await page.evaluate(`window.term.buffer.length`), 5); - await page.evaluate(`window.term.write('\\n\\n\\n\\n')`); + await writeSync(page, '\\n\\n\\n\\n'); assert.equal(await page.evaluate(`window.term.buffer.length`), 5); - await page.evaluate(`window.term.write('\\n')`); + await writeSync(page, '\\n'); assert.equal(await page.evaluate(`window.term.buffer.length`), 6); - await page.evaluate(`window.term.write('\\n\\n\\n\\n')`); + await writeSync(page, '\\n\\n\\n\\n'); assert.equal(await page.evaluate(`window.term.buffer.length`), 10); }); describe('getLine', () => { - it('invalid index', async function(): Promise { + it('invalid index', async () => { await openTerminal({ rows: 5 }); assert.equal(await page.evaluate(`window.term.buffer.getLine(-1)`), undefined); assert.equal(await page.evaluate(`window.term.buffer.getLine(5)`), undefined); }); - it('isWrapped', async function(): Promise { + it('isWrapped', async () => { await openTerminal({ cols: 5 }); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false); assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), false); - await page.evaluate(`window.term.write('abcde')`); + await writeSync(page, 'abcde'); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false); assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), false); - await page.evaluate(`window.term.write('f')`); + await writeSync(page, 'f'); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false); assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), true); }); - it('translateToString', async function(): Promise { + it('translateToString', async () => { await openTerminal({ cols: 5 }); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), ' '); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), ''); - await page.evaluate(`window.term.write('foo')`); + await writeSync(page, 'foo'); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), 'foo '); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); - await page.evaluate(`window.term.write('bar')`); + await writeSync(page, 'bar'); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), 'fooba'); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'fooba'); assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'r'); @@ -494,13 +494,13 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(false, 1, 3)`), 'oo'); }); - it('getCell', async function(): Promise { + it('getCell', async () => { await openTerminal({ cols: 5 }); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(-1)`), undefined); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(5)`), undefined); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), ''); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1); - await page.evaluate(`window.term.write('a文')`); + await writeSync(page, 'a文'); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), 'a'); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).char`), '文'); @@ -510,6 +510,20 @@ describe('API Integration Tests', function(): void { }); }); }); + + it('dispose', async () => { + await page.evaluate(` + window.term = new Terminal(); + window.term.dispose(); + `); + assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); + }); + + it('dispose (opened)', async () => { + await openTerminal(); + await page.evaluate(`window.term.dispose()`); + assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/test/api/TestUtils.ts b/test/api/TestUtils.ts new file mode 100644 index 000000000..d36748122 --- /dev/null +++ b/test/api/TestUtils.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import deepEqual = require('deep-equal'); + +export async function pollFor(page: puppeteer.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise): Promise { + if (preFn) { + await preFn(); + } + const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn(); + if (!deepEqual(result, val)) { + return new Promise(r => { + setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1); + }); + } +} + +export async function writeSync(page: puppeteer.Page, data: string): Promise { + await page.evaluate(` + window.ready = false; + window.term.write('${data}', () => window.ready = true); + `); + await pollFor(page, 'window.ready', true); +} + +export async function timeout(ms: number): Promise { + return new Promise(r => setTimeout(r, ms)); +} diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 44ef66bda..e848c547b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -49,7 +49,7 @@ declare module 'xterm' { /** * When enabled the cursor will be set to the beginning of the next line - * with every new line. This equivalent to sending '\r\n' for each '\n'. + * 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 @@ -72,6 +72,11 @@ declare module 'xterm' { */ cursorStyle?: 'block' | 'underline' | 'bar'; + /** + * The width of the cursor in CSS pixels when `cursorStyle` is set to 'bar'. + */ + cursorWidth?: number; + /** * Whether input should be disabled. */ @@ -82,6 +87,16 @@ declare module 'xterm' { */ drawBoldTextInBrightColors?: boolean; + /** + * The modifier key hold to multiply scroll speed. + */ + fastScrollModifier?: 'alt' | 'ctrl' | 'shift' | undefined; + + /** + * The scroll speed multiplier used for fast scrolling. + */ + fastScrollSensitivity?: number; + /** * The font size used to render text. */ @@ -138,6 +153,18 @@ declare module 'xterm' { */ 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; + /** * The type of renderer to use, this allows using the fallback DOM renderer * when canvas is too slow for the environment. The following features do @@ -173,6 +200,11 @@ declare module 'xterm' { */ scrollback?: number; + /** + * The scrolling speed multiplier used for adjusting normal scrolling speed. + */ + scrollSensitivity?: number; + /** * The size of tab stops in the terminal. */ @@ -269,7 +301,7 @@ declare module 'xterm' { /** * A callback that fires when the mouse hovers over a link for a moment. */ - tooltipCallback?: (event: MouseEvent, uri: string) => boolean | void; + tooltipCallback?: (event: MouseEvent, uri: string, location: IViewportRange) => boolean | void; /** * A callback that fires when the mouse leaves a link. Note that this can @@ -304,13 +336,14 @@ declare module 'xterm' { * An event that can be listened to. * @returns an `IDisposable` to stop listening. */ - export interface IEvent { - (listener: (e: T) => any): IDisposable; + 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. + * 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 { /** @@ -324,7 +357,8 @@ declare module 'xterm' { readonly isDisposed: boolean; /** - * The actual line index in the buffer at this point in time. + * 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; } @@ -352,12 +386,12 @@ declare module 'xterm' { /** * The element containing the terminal. */ - readonly element: HTMLElement; + readonly element: HTMLElement | undefined; /** * The textarea that accepts input for the terminal. */ - readonly textarea: HTMLTextAreaElement; + readonly textarea: HTMLTextAreaElement | undefined; /** * The number of rows in the terminal's viewport. Use @@ -404,6 +438,17 @@ declare module 'xterm' { */ constructor(options?: ITerminalOptions); + /** + * 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. @@ -420,7 +465,7 @@ declare module 'xterm' { onData: IEvent; /** - * Adds an event listener for a key is pressed. The event value contains the + * Adds an event listener for when a key is pressed. The event value contains the * string that will be sent in the data event as well as the DOM event that * triggered it. * @returns an `IDisposable` to stop listening. @@ -434,7 +479,7 @@ declare module 'xterm' { onLineFeed: IEvent; /** - * Adds an event listener for when a scroll occurs. The event value is the + * 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. */ @@ -567,6 +612,11 @@ declare module 'xterm' { * alt buffer is active, undefined is returned. * @param cursorYOffset The y position offset of the marker from the cursor. */ + registerMarker(cursorYOffset: number): IMarker; + + /** + * @deprecated use `registerMarker` instead. + */ addMarker(cursorYOffset: number): IMarker; /** @@ -592,7 +642,7 @@ declare module 'xterm' { /** * Selects text within the terminal. - * @param column The column the selection starts at.. + * @param column The column the selection starts at. * @param row The row the selection starts at. * @param length The length of the selection. */ @@ -842,6 +892,41 @@ declare module 'xterm' { 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; + } + /** * Represents a terminal buffer. */ @@ -866,7 +951,7 @@ declare module 'xterm' { /** * The line within the buffer where the top of the bottom page is (when - * fully scrolled down); + * fully scrolled down). */ readonly baseY: number; @@ -995,9 +1080,14 @@ declare module 'xterm' { * 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. + * 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; + + /** + * @deprecated use `registerMarker` instead. + */ addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; /** @@ -1014,9 +1104,14 @@ declare module 'xterm' { * 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. + * 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; + + /** + * @deprecated use `registerMarker` instead. + */ addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; /** @@ -1027,9 +1122,14 @@ declare module 'xterm' { * @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. + * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ + registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; + + /** + * @deprecated use `registerMarker` instead. + */ addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; /** @@ -1045,9 +1145,14 @@ declare module 'xterm' { * 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. + * 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; + + /** + * @deprecated use `registerMarker` instead. + */ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; } } diff --git a/yarn.lock b/yarn.lock index e4d8726b8..22e3acc6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,46 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" + integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.6.3": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" + integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w== + dependencies: + "@babel/types" "^7.6.3" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + +"@babel/helper-function-name@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" + integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== + dependencies: + "@babel/helper-get-function-arity" "^7.0.0" + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-get-function-arity@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" + integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -18,6 +58,44 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.6.3": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81" + integrity sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A== + +"@babel/template@^7.1.0", "@babel/template@^7.4.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" + integrity sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.0" + +"@babel/traverse@^7.4.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.3.tgz#66d7dba146b086703c0fb10dd588b7364cec47f9" + integrity sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.3" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.6.3" + "@babel/types" "^7.6.3" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/types@^7.0.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.0", "@babel/types@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09" + integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@fimbul/bifrost@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@fimbul/bifrost/-/bifrost-0.11.0.tgz#83cacc21464198b12e3cc1c2204ae6c6d7afd158" @@ -52,6 +130,11 @@ resolved "https://registry.yarnpkg.com/@types/cli-table/-/cli-table-0.3.0.tgz#f1857156bf5fd115c6a2db260ba0be1f8fc5671c" integrity sha512-QnZUISJJXyhyD6L1e5QwXDV/A5i2W1/gl6D6YMc8u0ncPepbv/B4w3S+izVvtAg60m6h+JP09+Y/0zF2mojlFQ== +"@types/deep-equal@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03" + integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg== + "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -439,11 +522,23 @@ app-root-path@^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== +append-transform@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" + integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== + dependencies: + default-require-extensions "^2.0.0" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= + are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -489,6 +584,11 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + asn1.js@^4.0.0: version "4.10.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" @@ -816,6 +916,16 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +caching-transform@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-3.0.2.tgz#601d46b91eca87687a281e71cef99791b0efca70" + integrity sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w== + dependencies: + hasha "^3.0.0" + make-dir "^2.0.0" + package-hash "^3.0.0" + write-file-atomic "^2.4.2" + camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -1075,6 +1185,11 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@~2.20.3: + 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== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -1134,6 +1249,13 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +convert-source-map@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -1197,6 +1319,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-spawn@^4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1293,7 +1423,7 @@ debug@^3.1.0: dependencies: ms "2.0.0" -debug@^4.0.1, debug@^4.1.0: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -1329,6 +1459,18 @@ deep-eql@^0.1.3: dependencies: type-detect "0.1.1" +deep-equal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.0.tgz#3103cdf8ab6d32cf4a8df7865458f2b8d33f3745" + integrity sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -1339,6 +1481,13 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +default-require-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" + integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= + dependencies: + strip-bom "^3.0.0" + defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" @@ -1536,6 +1685,13 @@ errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es-abstract@^1.5.1: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" @@ -1557,6 +1713,11 @@ es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-error@^4.0.1: + version "4.1.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.6" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" @@ -1878,7 +2039,7 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^2.1.0: +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== @@ -1921,6 +2082,14 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +foreground-child@^1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" + integrity sha1-T9ca0t/elnibmApcCilZN8svXOk= + dependencies: + cross-spawn "^4" + signal-exit "^3.0.0" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -2097,6 +2266,11 @@ global-modules-path@^2.1.0: resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.3.0.tgz#b0e2bac6beac39745f7db5c59d26a36a0b94f7dc" integrity sha512-HchvMJNYh9dGSCy8pOQ2O8u/hoXaL+0XhnrwH0RyLiSXMMTl9W3N6KUU73+JFOg5PGjtzl6VZzUQsnrpm7Szag== +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: version "4.2.2" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" @@ -2107,6 +2281,17 @@ growl@1.10.5: resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== +handlebars@^4.1.2: + version "4.5.1" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.1.tgz#8a01c382c180272260d07f2d1aa3ae745715c7ba" + integrity sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -2189,6 +2374,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasha@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-3.0.0.tgz#52a32fab8569d41ca69a61ff1a214f8eb7c8bd39" + integrity sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk= + dependencies: + is-stream "^1.0.1" + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -2203,6 +2395,11 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hosted-git-info@^2.1.4: + version "2.8.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" + integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== + html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" @@ -2387,6 +2584,16 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +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-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" @@ -2525,7 +2732,7 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" -is-stream@^1.1.0: +is-stream@^1.0.1, 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= @@ -2579,6 +2786,58 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +istanbul-lib-coverage@^2.0.3, istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-hook@^2.0.3: + version "2.0.7" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz#c95695f383d4f8f60df1f04252a9550e15b5b133" + integrity sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA== + dependencies: + append-transform "^1.0.0" + +istanbul-lib-instrument@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-report@^2.0.4: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + dependencies: + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" + +istanbul-lib-source-maps@^3.0.2: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^2.1.1: + version "2.2.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" + integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== + dependencies: + handlebars "^4.1.2" + 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" @@ -2634,7 +2893,12 @@ jsdom@^11.11.0: ws "^4.0.0" xml-name-validator "^3.0.0" -json-parse-better-errors@^1.0.2: +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== @@ -2739,6 +3003,16 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -2778,6 +3052,11 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +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" @@ -2788,6 +3067,11 @@ lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== +lodash@^4.17.13: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + log-symbols@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -2821,7 +3105,14 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -make-dir@^2.0.0: +make-dir@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== @@ -2910,6 +3201,13 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -3021,6 +3319,11 @@ minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + minipass@^2.2.1, minipass@^2.3.5: version "2.5.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.5.0.tgz#dddb1d001976978158a05badfcbef4a771612857" @@ -3128,12 +3431,7 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" - integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== - -nan@^2.12.1: +nan@^2.12.1, nan@^2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -3169,7 +3467,7 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.5.0, neo-async@^2.6.1: +neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== @@ -3232,12 +3530,12 @@ node-pre-gyp@^0.12.0: semver "^5.3.0" tar "^4" -node-pty@0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.6.tgz#bff6148c9c5836ca7e73c7aaaec067dcbdac2f7b" - integrity sha512-ECzKUB7KkAFZ0cjyjMXp5WLJ+7YIZ1xnNmiiegOI6WdDaKABUNV5NbB1Dw9MXD4KrZipWII0wQ7RGZ6StU/7jA== +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== dependencies: - nan "2.10.0" + nan "^2.14.0" nopt@^4.0.1: version "4.0.1" @@ -3247,6 +3545,16 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -3299,6 +3607,36 @@ nwsapi@^2.0.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.4.tgz#dc79040a5f77b97716dc79565fc7fc3ef7d50570" integrity sha512-Zt6HRR6RcJkuj5/N9zeE7FN6YitRW//hK2wTOwX274IBphbY3Zf5+yn5mZ9v/SzAOTMjQNxZf9KkmPLWn0cV4g== +nyc@13: + version "13.3.0" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-13.3.0.tgz#da4dbe91a9c8b9ead3f4f3344c76f353e3c78c75" + integrity sha512-P+FwIuro2aFG6B0Esd9ZDWUd51uZrAEoGutqZxzrVmYl3qSfkLgcQpBPBjtDFsUQLFY1dvTQJPOyeqr8S9GF8w== + dependencies: + archy "^1.0.0" + arrify "^1.0.1" + caching-transform "^3.0.1" + convert-source-map "^1.6.0" + find-cache-dir "^2.0.0" + find-up "^3.0.0" + foreground-child "^1.5.6" + glob "^7.1.3" + istanbul-lib-coverage "^2.0.3" + istanbul-lib-hook "^2.0.3" + istanbul-lib-instrument "^3.1.0" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.2" + istanbul-reports "^2.1.1" + make-dir "^1.3.0" + merge-source-map "^1.1.0" + resolve-from "^4.0.0" + rimraf "^2.6.3" + signal-exit "^3.0.2" + spawn-wrap "^1.4.2" + test-exclude "^5.1.0" + uuid "^3.3.2" + yargs "^12.0.5" + yargs-parser "^11.1.1" + oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -3318,7 +3656,12 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-keys@^1.0.11, object-keys@^1.0.12: +object-is@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" + integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY= + +object-keys@^1.0.11, 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== @@ -3381,6 +3724,14 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + optionator@^0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" @@ -3398,7 +3749,7 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= -os-homedir@^1.0.0: +os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= @@ -3487,6 +3838,16 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-3.0.0.tgz#50183f2d36c9e3e528ea0a8605dff57ce976f88e" + integrity sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA== + dependencies: + graceful-fs "^4.1.15" + hasha "^3.0.0" + lodash.flattendeep "^4.4.0" + release-zalgo "^1.0.0" + pako@~1.0.5: version "1.0.10" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" @@ -3513,6 +3874,14 @@ parse-asn1@^5.0.0: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + parse5@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" @@ -3565,11 +3934,23 @@ path-parse@^1.0.5: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" integrity sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME= +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== + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -3596,6 +3977,11 @@ picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -3818,6 +4204,23 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -3862,6 +4265,20 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexp.prototype.flags@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz#6b30724e306a27833eeb171b66ac8890ba37e41c" + integrity sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA== + dependencies: + define-properties "^1.1.2" + +release-zalgo@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" + integrity sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA= + dependencies: + es6-error "^4.0.1" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -3946,6 +4363,11 @@ resolve-from@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" integrity sha1-six699nWiBvItuZTM17rywoYh0g= +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -3956,6 +4378,13 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= +resolve@^1.10.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" + integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== + dependencies: + path-parse "^1.0.6" + resolve@^1.3.2: version "1.8.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" @@ -3976,7 +4405,7 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: +rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -4053,7 +4482,7 @@ seed-random@2.2.0: resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ= -semver@^5.3.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -4238,7 +4667,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= -source-map@^0.5.6: +source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -4248,6 +4677,44 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +spawn-wrap@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.3.tgz#81b7670e170cca247d80bf5faf0cfb713bdcf848" + integrity sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw== + dependencies: + foreground-child "^1.5.6" + mkdirp "^0.5.0" + os-homedir "^1.0.1" + rimraf "^2.6.2" + signal-exit "^3.0.2" + which "^1.3.0" + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -4406,6 +4873,11 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -4437,6 +4909,13 @@ supports-color@^5.4.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" @@ -4484,6 +4963,16 @@ terser@^4.1.2: source-map "~0.6.1" source-map-support "~0.5.12" +test-exclude@^5.1.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -4526,6 +5015,11 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -4724,11 +5218,24 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.5, typescript@^3.5.1: +typescript@3.7: + version "3.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" + integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== + +typescript@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.1.tgz#ba72a6a600b2158139c5dd8850f700e231464202" integrity sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw== +uglify-js@^3.1.4: + version "3.6.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.5.tgz#b0ee796d2ae7e25672e04f65629b997cd4b30bd6" + integrity sha512-7L3W+Npia1OCr5Blp4/Vw83tK1mu5gnoIURtT1fUVfQ3Kf8WStWV6NJz0fdoBJZls0KlweruRTLVe6XLafmy5g== + dependencies: + commander "~2.20.3" + source-map "~0.6.1" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -4830,11 +5337,24 @@ uuid@^3.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== +uuid@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" + integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== + v8-compile-cache@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw== +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -4962,7 +5482,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@1.3.1, which@^1.2.9: +which@1.3.1, which@^1.2.9, which@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -4999,6 +5519,11 @@ winston@^3.2.1: triple-beam "^1.3.0" winston-transport "^4.3.0" +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -5024,6 +5549,15 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write-file-atomic@^2.4.2: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + ws@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289"