diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index c09a29bf..9c83cc4c 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,13 +1,15 @@
{
"name": "xterm.js",
- "image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
+ "image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm",
"features": {
- "ghcr.io/devcontainers/features/node:1": {} // yarn
+ "ghcr.io/devcontainers/features/node:1": {
+ "version": 18
+ } // yarn
},
"forwardPorts": [
3000
],
- "postCreateCommand": "yarn install",
+ "postCreateCommand": "yarn install && yarn setup",
"customizations": {
"vscode": {
"extensions": [
diff --git a/.eslintrc.json b/.eslintrc.json
index 9ba50120..5e49b021 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -18,6 +18,8 @@
"addons/addon-attach/test/tsconfig.json",
"addons/addon-canvas/src/tsconfig.json",
"addons/addon-canvas/test/tsconfig.json",
+ "addons/addon-clipboard/src/tsconfig.json",
+ "addons/addon-clipboard/test/tsconfig.json",
"addons/addon-fit/src/tsconfig.json",
"addons/addon-fit/test/tsconfig.json",
"addons/addon-image/src/tsconfig.json",
@@ -42,6 +44,8 @@
},
"ignorePatterns": [
"addons/*/src/third-party/*.ts",
+ "out/*",
+ "out-test/*",
"**/inwasm-sdks/*",
"**/typings/*.d.ts",
"**/node_modules",
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0a64b6a7..c893d8a7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,8 @@ jobs:
./addons/addon-attach/out-test/* \
./addons/addon-canvas/out/* \
./addons/addon-canvas/out-test/* \
+ ./addons/addon-clipboard/out/* \
+ ./addons/addon-clipboard/out-test/* \
./addons/addon-fit/out/* \
./addons/addon-fit/out-test/* \
./addons/addon-image/out/* \
@@ -148,40 +150,6 @@ jobs:
- name: Unit tests
run: yarn test-unit --forbid-only
- test-unit:
- needs: build
- timeout-minutes: 20
- strategy:
- matrix:
- node-version: [16]
- runs-on: [ubuntu, macos, windows]
- runs-on: ${{ matrix.runs-on }}-latest
- steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}.x
- uses: actions/setup-node@v3
- with:
- node-version: ${{ matrix.node-version }}.x
- cache: 'yarn'
- - name: Install dependencies
- run: |
- yarn --frozen-lockfile
- yarn install-addons
- - uses: actions/download-artifact@v3
- with:
- name: build-artifacts
- - name: Unzip artifacts
- shell: bash
- run: |
- if [ "$RUNNER_OS" == "Windows" ]; then
- pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
- else
- unzip -o compressed-build.zip
- fi
- ls -R
- - name: Unit tests
- run: yarn test-unit --forbid-only
-
test-api-parallel:
timeout-minutes: 20
strategy:
diff --git a/.nvmrc b/.nvmrc
index b6a7d89c..3c032078 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-16
+18
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 5dbd01ce..eaa5e12e 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -61,7 +61,7 @@
"runtimeExecutable": "npm",
"runtimeArgs": ["start"],
"stopOnEntry": true,
- "runtimeVersion": "16",
+ "runtimeVersion": "18",
"serverReadyAction": {
"action": "openExternally",
"pattern": "App listening to (http://.*?:[0-9]+)"
diff --git a/README.md b/README.md
index 7514e76c..d9392de3 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running:
```bash
-npm install xterm
+npm install @xterm/xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `
` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
@@ -30,8 +30,8 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
-
-
+
+
@@ -78,6 +78,7 @@ The xterm.js team maintains the following addons, but anyone can build them:
- [`@xterm/addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-attach): Attaches to a server running a process via a websocket
- [`@xterm/addon-canvas`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-canvas): Renders xterm.js using a `canvas` element's 2d context
+- [`@xterm/addon-clipboard`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard): Access the browser's clipboard
- [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit): Fits the terminal to the containing element
- [`@xterm/addon-image`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image): Adds image support
- [`@xterm/addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-search): Adds search functionality
@@ -113,7 +114,7 @@ All current and past releases are available on this repo's [Releases page](https
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
```bash
-npm install -S xterm@beta
+npm install -S @xterm/xterm@beta
```
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
@@ -222,6 +223,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Cloudtutor.io**](https://cloudtutor.io): innovative online learning platform that offers users access to an interactive lab.
- [**Helix Editor Playground**](https://github.com/tomgroenwoldt/helix-editor-playground): Online playground for the terminal based helix editor.
- [**Coder**](https://github.com/coder/coder): Self-Hosted Remote Development Environments
+- [**Wave Terminal**](https://waveterm.dev): An open-source, ai-native, terminal built for seamless workflows.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
diff --git a/addons/addon-attach/package.json b/addons/addon-attach/package.json
index 94b483dd..71b1188d 100644
--- a/addons/addon-attach/package.json
+++ b/addons/addon-attach/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-attach",
- "version": "0.9.0",
+ "version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-attach/webpack.config.js b/addons/addon-attach/webpack.config.js
index 599bb142..3599a977 100644
--- a/addons/addon-attach/webpack.config.js
+++ b/addons/addon-attach/webpack.config.js
@@ -25,7 +25,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-canvas/package.json b/addons/addon-canvas/package.json
index 9aabb001..2ca5d163 100644
--- a/addons/addon-canvas/package.json
+++ b/addons/addon-canvas/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-canvas",
- "version": "0.5.0",
+ "version": "0.7.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-canvas/src/BaseRenderLayer.ts b/addons/addon-canvas/src/BaseRenderLayer.ts
index e7e23400..cd3cfa1f 100644
--- a/addons/addon-canvas/src/BaseRenderLayer.ts
+++ b/addons/addon-canvas/src/BaseRenderLayer.ts
@@ -8,7 +8,7 @@ import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
-import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
+import { allowRescaling, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
@@ -365,6 +365,8 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
*/
protected _drawChars(cell: ICellData, x: number, y: number): void {
const chars = cell.getChars();
+ const code = cell.getCode();
+ const width = cell.getWidth();
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y, this._deviceCellWidth);
if (!this._charAtlas) {
@@ -400,6 +402,16 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
this._bitmapGenerator[glyph.texturePage]!.refresh();
this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version;
}
+
+ // Reduce scale horizontally for wide glyphs printed in cells that would overlap with the
+ // following cell (ie. the width is not 2).
+ let renderWidth = glyph.size.x;
+ if (this._optionsService.rawOptions.rescaleOverlappingGlyphs) {
+ if (allowRescaling(code, width, glyph.size.x, this._deviceCellWidth)) {
+ renderWidth = this._deviceCellWidth - 1; // - 1 to improve readability
+ }
+ }
+
this._ctx.drawImage(
this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas,
glyph.texturePosition.x,
@@ -408,7 +420,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
glyph.size.y,
x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x,
y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y,
- glyph.size.x,
+ renderWidth,
glyph.size.y
);
this._ctx.restore();
diff --git a/addons/addon-canvas/src/CanvasAddon.ts b/addons/addon-canvas/src/CanvasAddon.ts
index 7f7f679b..4d32b021 100644
--- a/addons/addon-canvas/src/CanvasAddon.ts
+++ b/addons/addon-canvas/src/CanvasAddon.ts
@@ -37,7 +37,7 @@ export class CanvasAddon extends Disposable implements ITerminalAddon , ICanvasA
const coreService = core.coreService;
const optionsService = core.optionsService;
const screenElement = core.screenElement!;
- const linkifier = core.linkifier2;
+ const linkifier = core.linkifier!;
const unsafeCore = core as any;
const bufferService: IBufferService = unsafeCore._bufferService;
diff --git a/addons/addon-canvas/src/CanvasRenderer.ts b/addons/addon-canvas/src/CanvasRenderer.ts
index 40b89546..148ae736 100644
--- a/addons/addon-canvas/src/CanvasRenderer.ts
+++ b/addons/addon-canvas/src/CanvasRenderer.ts
@@ -10,7 +10,7 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
-import { Disposable, toDisposable } from 'common/Lifecycle';
+import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm';
import { CursorRenderLayer } from './CursorRenderLayer';
@@ -22,6 +22,7 @@ import { IRenderLayer } from './Types';
export class CanvasRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
+ private _observerDisposable = this.register(new MutableDisposable());
public dimensions: IRenderDimensions;
@@ -60,7 +61,11 @@ export class CanvasRenderer extends Disposable implements IRenderer {
this._devicePixelRatio = this._coreBrowserService.dpr;
this._updateDimensions();
- this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
+ this._observerDisposable.value = observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
+ this.register(this._coreBrowserService.onWindowChange(w => {
+ this._observerDisposable.value = observeDevicePixelDimensions(this._renderLayers[0].canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
+ }));
+
this.register(toDisposable(() => {
for (const l of this._renderLayers) {
l.dispose();
diff --git a/addons/addon-canvas/test/CanvasRenderer.test.ts b/addons/addon-canvas/test/CanvasRenderer.test.ts
index 2782081c..c8b35c7a 100644
--- a/addons/addon-canvas/test/CanvasRenderer.test.ts
+++ b/addons/addon-canvas/test/CanvasRenderer.test.ts
@@ -28,5 +28,10 @@ test.describe('Canvas Renderer Integration Tests', () => {
test.skip(({ browserName }) => browserName === 'webkit');
injectSharedRendererTests(ctxWrapper);
- injectSharedRendererTestsStandalone(ctxWrapper);
+ injectSharedRendererTestsStandalone(ctxWrapper, async () => {
+ await ctx.page.evaluate(`
+ window.addon = new window.CanvasAddon(true);
+ window.term.loadAddon(window.addon);
+ `);
+ });
});
diff --git a/addons/addon-canvas/webpack.config.js b/addons/addon-canvas/webpack.config.js
index 9daa08f9..e0c7fde2 100644
--- a/addons/addon-canvas/webpack.config.js
+++ b/addons/addon-canvas/webpack.config.js
@@ -33,7 +33,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-clipboard/.gitignore b/addons/addon-clipboard/.gitignore
new file mode 100644
index 00000000..3063f07d
--- /dev/null
+++ b/addons/addon-clipboard/.gitignore
@@ -0,0 +1,2 @@
+lib
+node_modules
diff --git a/addons/addon-clipboard/.npmignore b/addons/addon-clipboard/.npmignore
new file mode 100644
index 00000000..b203232a
--- /dev/null
+++ b/addons/addon-clipboard/.npmignore
@@ -0,0 +1,29 @@
+# Blacklist - exclude everything except npm defaults such as LICENSE, etc
+*
+!*/
+
+# Whitelist - lib/
+!lib/**/*.d.ts
+
+!lib/**/*.js
+!lib/**/*.js.map
+
+!lib/**/*.css
+
+# Whitelist - src/
+!src/**/*.ts
+!src/**/*.d.ts
+
+!src/**/*.js
+!src/**/*.js.map
+
+!src/**/*.css
+
+# Blacklist - src/ test files
+src/**/*.test.ts
+src/**/*.test.d.ts
+src/**/*.test.js
+src/**/*.test.js.map
+
+# Whitelist - typings/
+!typings/*.d.ts
diff --git a/addons/addon-clipboard/LICENSE b/addons/addon-clipboard/LICENSE
new file mode 100644
index 00000000..b6c38b15
--- /dev/null
+++ b/addons/addon-clipboard/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2023, The xterm.js authors (https://github.com/xtermjs/xterm.js)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/addons/addon-clipboard/README.md b/addons/addon-clipboard/README.md
new file mode 100644
index 00000000..7fef805c
--- /dev/null
+++ b/addons/addon-clipboard/README.md
@@ -0,0 +1,53 @@
+## @xterm/addon-clipboard
+
+An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables
+accessing the system clipboard. This addon requires xterm.js v4+.
+
+### Install
+
+```bash
+npm install --save @xterm/addon-clipboard
+```
+
+### Usage
+
+```ts
+import { Terminal } from 'xterm';
+import { ClipboardAddon } from '@xterm/addon-clipboard';
+
+const terminal = new Terminal();
+const clipboardAddon = new ClipboardAddon();
+terminal.loadAddon(clipboardAddon);
+```
+
+To use a custom clipboard provider
+
+```ts
+import { Terminal } from '@xterm/xterm';
+import { ClipboardAddon, IClipboardProvider, ClipboardSelectionType } from '@xterm/addon-clipboard';
+
+function b64Encode(data: string): string {
+ // Base64 encode impl
+}
+
+function b64Decode(data: string): string {
+ // Base64 decode impl
+}
+
+class MyCustomClipboardProvider implements IClipboardProvider {
+ private _data: string
+ public readText(selection: ClipboardSelectionType): Promise {
+ return Promise.resolve(b64Encode(this._data));
+ }
+ public writeText(selection: ClipboardSelectionType, data: string): Promise {
+ this._data = b64Decode(data);
+ return Promise.resolve();
+ }
+}
+
+const terminal = new Terminal();
+const clipboardAddon = new ClipboardAddon(new MyCustomClipboardProvider());
+terminal.loadAddon(clipboardAddon);
+```
+
+See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-clipboard/typings/addon-clipboard.d.ts) for more advanced usage.
diff --git a/addons/addon-clipboard/package.json b/addons/addon-clipboard/package.json
new file mode 100644
index 00000000..af433f93
--- /dev/null
+++ b/addons/addon-clipboard/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@xterm/addon-clipboard",
+ "version": "0.1.0",
+ "author": {
+ "name": "The xterm.js authors",
+ "url": "https://xtermjs.org/"
+ },
+ "main": "lib/addon-clipboard.js",
+ "types": "typings/addon-clipboard.d.ts",
+ "repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard",
+ "license": "MIT",
+ "keywords": [
+ "terminal",
+ "xterm",
+ "xterm.js"
+ ],
+ "scripts": {
+ "build": "../../node_modules/.bin/tsc -p .",
+ "prepackage": "npm run build",
+ "package": "../../node_modules/.bin/webpack",
+ "prepublishOnly": "npm run package"
+ },
+ "peerDependencies": {
+ "@xterm/xterm": "^5.4.0"
+ },
+ "dependencies": {
+ "js-base64": "^3.7.5"
+ }
+}
diff --git a/addons/addon-clipboard/src/ClipboardAddon.ts b/addons/addon-clipboard/src/ClipboardAddon.ts
new file mode 100644
index 00000000..58425924
--- /dev/null
+++ b/addons/addon-clipboard/src/ClipboardAddon.ts
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import type { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm';
+import { type IClipboardProvider, ClipboardSelectionType, type IBase64 } from '@xterm/addon-clipboard';
+import { Base64 as JSBase64 } from 'js-base64';
+
+export class ClipboardAddon implements ITerminalAddon {
+ private _terminal?: Terminal;
+ private _disposable?: IDisposable;
+
+ constructor(
+ private _base64: IBase64 = new Base64(),
+ private _provider: IClipboardProvider = new BrowserClipboardProvider()
+ ) {}
+
+ public activate(terminal: Terminal): void {
+ this._terminal = terminal;
+ this._disposable = terminal.parser.registerOscHandler(52, data => this._setOrReportClipboard(data));
+ }
+
+ public dispose(): void {
+ return this._disposable?.dispose();
+ }
+
+ private _readText(sel: ClipboardSelectionType, data: string): void {
+ const b64 = this._base64.encodeText(data);
+ this._terminal?.input(`\x1b]52;${sel};${b64}\x07`, false);
+ }
+
+ private _setOrReportClipboard(data: string): boolean | Promise {
+ const args = data.split(';');
+ if (args.length < 2) {
+ return true;
+ }
+
+ const pc = args[0] as ClipboardSelectionType;
+ const pd = args[1];
+ if (pd === '?') {
+ const text = this._provider.readText(pc);
+
+ // Report clipboard
+ if (text instanceof Promise) {
+ return text.then((data) => {
+ this._readText(pc, data);
+ return true;
+ });
+ }
+
+ this._readText(pc, text);
+ return true;
+ }
+
+ // Clear clipboard if text is not a base64 encoded string.
+ let text = '';
+ try {
+ text = this._base64.decodeText(pd);
+ } catch {}
+
+
+ const result = this._provider.writeText(pc, text);
+ if (result instanceof Promise) {
+ return result.then(() => true);
+ }
+
+ return true;
+ }
+}
+
+export class BrowserClipboardProvider implements IClipboardProvider {
+ public async readText(selection: ClipboardSelectionType): Promise {
+ if (selection !== 'c') {
+ return Promise.resolve('');
+ }
+ return navigator.clipboard.readText();
+ }
+
+ public async writeText(selection: ClipboardSelectionType, text: string): Promise {
+ if (selection !== 'c') {
+ return Promise.resolve();
+ }
+ return navigator.clipboard.writeText(text);
+ }
+}
+
+export class Base64 implements IBase64 {
+ public encodeText(data: string): string {
+ return JSBase64.encode(data);
+ }
+ public decodeText(data: string): string {
+ const text = JSBase64.decode(data);
+ if (!JSBase64.isValid(data) || JSBase64.encode(text) !== data) {
+ return '';
+ }
+ return text;
+ }
+}
diff --git a/addons/addon-clipboard/src/tsconfig.json b/addons/addon-clipboard/src/tsconfig.json
new file mode 100644
index 00000000..b6107280
--- /dev/null
+++ b/addons/addon-clipboard/src/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es2021",
+ "lib": [
+ "dom",
+ "es2015"
+ ],
+ "rootDir": ".",
+ "outDir": "../out",
+ "sourceMap": true,
+ "removeComments": true,
+ "strict": true,
+ "types": [
+ "../../../node_modules/@types/mocha"
+ ],
+ "paths": {
+ "browser/*": [
+ "../../../src/browser/*"
+ ],
+ "@xterm/addon-clipboard": [
+ "../typings/addon-clipboard.d.ts"
+ ]
+ }
+ },
+ "include": [
+ "./**/*",
+ "../../../typings/xterm.d.ts"
+ ],
+ "references": [
+ {
+ "path": "../../../src/browser"
+ }
+ ]
+}
diff --git a/addons/addon-clipboard/test/ClipboardAddon.api.ts b/addons/addon-clipboard/test/ClipboardAddon.api.ts
new file mode 100644
index 00000000..4ef768e8
--- /dev/null
+++ b/addons/addon-clipboard/test/ClipboardAddon.api.ts
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { assert } from 'chai';
+import { openTerminal, launchBrowser, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
+import { Browser, BrowserContext, Page } from '@playwright/test';
+import { beforeEach } from 'mocha';
+
+const APP = 'http://127.0.0.1:3001/test';
+
+let browser: Browser;
+let context: BrowserContext;
+let page: Page;
+const width = 800;
+const height = 600;
+
+describe('ClipboardAddon', () => {
+ before(async function (): Promise {
+ browser = await launchBrowser({
+ // Enable clipboard access in firefox, mainly for readText
+ firefoxUserPrefs: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'dom.events.testing.asyncClipboard': true,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'dom.events.asyncClipboard.readText': true
+ }
+ });
+ context = await browser.newContext();
+ if (getBrowserType().name() !== 'webkit') {
+ // Enable clipboard access in chromium without user gesture
+ context.grantPermissions(['clipboard-read', 'clipboard-write']);
+ }
+ page = await context.newPage();
+ await page.setViewportSize({ width, height });
+ await page.goto(APP);
+ await openTerminal(page);
+ await page.evaluate(`
+ window.clipboardAddon = new ClipboardAddon();
+ window.term.loadAddon(window.clipboardAddon);
+ `);
+ });
+
+ after(() => {
+ browser.close();
+ });
+
+ beforeEach(async () => {
+ await page.evaluate(`window.term.reset()`);
+ });
+
+ const testDataEncoded = 'aGVsbG8gd29ybGQ=';
+ const testDataDecoded = 'hello world';
+
+ describe('write data', async function (): Promise {
+ it('simple string', async () => {
+ await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
+ assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
+ });
+ it('invalid base64 string', async () => {
+ await writeSync(page, `\x1b]52;c;${testDataEncoded}invalid\x07`);
+ assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
+ });
+ it('empty string', async () => {
+ await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
+ await writeSync(page, `\x1b]52;c;\x07`);
+ assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
+ });
+ });
+
+ describe('read data', async function (): Promise {
+ it('simple string', async () => {
+ await page.evaluate(`
+ window.data = [];
+ window.term.onData(e => data.push(e));
+ `);
+ await page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
+ await writeSync(page, `\x1b]52;c;?\x07`);
+ assert.deepEqual(await page.evaluate('window.data'), [`\x1b]52;c;${testDataEncoded}\x07`]);
+ });
+ it('clear clipboard', async () => {
+ await writeSync(page, `\x1b]52;c;!\x07`);
+ await writeSync(page, `\x1b]52;c;?\x07`);
+ assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
+ });
+ });
+});
diff --git a/addons/addon-clipboard/test/tsconfig.json b/addons/addon-clipboard/test/tsconfig.json
new file mode 100644
index 00000000..67ad42b7
--- /dev/null
+++ b/addons/addon-clipboard/test/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es2021",
+ "lib": [
+ "es2015"
+ ],
+ "rootDir": ".",
+ "outDir": "../out-test",
+ "sourceMap": true,
+ "removeComments": true,
+ "strict": true,
+ "types": [
+ "../../../node_modules/@types/mocha",
+ "../../../node_modules/@types/node",
+ "../../../out-test/api/TestUtils"
+ ]
+ },
+ "include": [
+ "./**/*",
+ "../../../typings/xterm.d.ts"
+ ]
+}
diff --git a/addons/addon-clipboard/tsconfig.json b/addons/addon-clipboard/tsconfig.json
new file mode 100644
index 00000000..2d820dd1
--- /dev/null
+++ b/addons/addon-clipboard/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "files": [],
+ "include": [],
+ "references": [
+ { "path": "./src" },
+ { "path": "./test" }
+ ]
+}
diff --git a/addons/addon-clipboard/typings/addon-clipboard.d.ts b/addons/addon-clipboard/typings/addon-clipboard.d.ts
new file mode 100644
index 00000000..f37748fa
--- /dev/null
+++ b/addons/addon-clipboard/typings/addon-clipboard.d.ts
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { Terminal, ITerminalAddon } from '@xterm/xterm';
+
+declare module '@xterm/addon-clipboard' {
+ /**
+ * An xterm.js addon that enables accessing the system clipboard from
+ * xterm.js.
+ */
+ export class ClipboardAddon implements ITerminalAddon {
+ /**
+ * Creates a new clipboard addon.
+ */
+ constructor(provider?: IClipboardProvider);
+
+ /**
+ * Activates the addon
+ * @param terminal The terminal the addon is being loaded in.
+ */
+ public activate(terminal: Terminal): void;
+
+ /**
+ * Disposes the addon.
+ */
+ public dispose(): void
+ }
+
+ /**
+ * Clipboard selection type. This is used to specify which selection buffer to
+ * read or write to.
+ * - SYSTEM `c`: The system clipboard.
+ * - PRIMARY `p`: The primary clipboard. This is provided for compatibility
+ * with Linux X11.
+ */
+ export const enum ClipboardSelectionType {
+ SYSTEM = 'c',
+ PRIMARY = 'p',
+ }
+
+ export interface IBase64 {
+ /**
+ * Converts a utf-8 string to a base64 string.
+ * @param data The utf-8 string to convert to base64 string.
+ */
+ encodeText(data: string): string;
+
+ /**
+ * Converts a base64 string to a utf-8 string.
+ * @param data The base64 string to convert to utf-8 string.
+ * @throws An error if the input is not valid base64.
+ */
+ decodeText(data: string): string;
+ }
+
+ /**
+ * A default Base64 encoding and decoding type.
+ **/
+ export class Base64 implements IBase64 {
+ /**
+ * Converts a utf-8 string to a base64 string.
+ * @param data The utf-8 string to convert to base64 string.
+ */
+ public encodeText(data: string): string;
+
+ /**
+ * Converts a base64 string to a utf-8 string.
+ * @param data The base64 string to convert to utf-8 string.
+ * @throws An error if the input is not valid base64.
+ */
+ public decodeText(data: string): string;
+ }
+
+ export interface IClipboardProvider {
+ /**
+ * Gets the clipboard content.
+ * @param selection The clipboard selection to read.
+ * @returns A promise that resolves with clipboard selection data.
+ */
+ readText(selection: ClipboardSelectionType): string | Promise;
+
+ /**
+ * Sets the clipboard content.
+ * @param selection The clipboard selection to set.
+ * @param data The clipboard text to write.
+ */
+ writeText(selection: ClipboardSelectionType, text: string): void | Promise;
+ }
+
+ /**
+ * The clipboard provider interface that enables xterm.js to access the system clipboard.
+ */
+ export class BrowserClipboardProvider implements IClipboardProvider{
+ /**
+ * Reads text from the clipboard.
+ * @param selection The selection type to read from.
+ * @returns A promise that resolves with the text from the clipboard.
+ */
+ public readText(selection: ClipboardSelectionType): Promise;
+
+ /**
+ * Writes text to the clipboard.
+ * @param selection The selection type to write to.
+ * @param data The text to write to the clipboard.
+ * @returns A promise that resolves when the text has been written to the clipboard.
+ */
+ public writeText(selection: ClipboardSelectionType, data: string): Promise;
+ }
+}
diff --git a/addons/addon-clipboard/webpack.config.js b/addons/addon-clipboard/webpack.config.js
new file mode 100644
index 00000000..c00191ab
--- /dev/null
+++ b/addons/addon-clipboard/webpack.config.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+const path = require('path');
+
+const addonName = 'ClipboardAddon';
+const mainFile = 'addon-clipboard.js';
+
+module.exports = {
+ entry: `./out/${addonName}.js`,
+ devtool: 'source-map',
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ use: ["source-map-loader"],
+ enforce: "pre",
+ exclude: /node_modules/
+ }
+ ]
+ },
+ output: {
+ filename: mainFile,
+ path: path.resolve('./lib'),
+ library: addonName,
+ libraryTarget: 'umd'
+ },
+ mode: 'production'
+};
diff --git a/addons/addon-clipboard/yarn.lock b/addons/addon-clipboard/yarn.lock
new file mode 100644
index 00000000..01d54e36
--- /dev/null
+++ b/addons/addon-clipboard/yarn.lock
@@ -0,0 +1,8 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+js-base64@^3.7.5:
+ version "3.7.7"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79"
+ integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==
diff --git a/addons/addon-fit/package.json b/addons/addon-fit/package.json
index 585f3621..cacf31dd 100644
--- a/addons/addon-fit/package.json
+++ b/addons/addon-fit/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-fit",
- "version": "0.8.0",
+ "version": "0.10.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-fit/test/FitAddon.api.ts b/addons/addon-fit/test/FitAddon.api.ts
index 5611972e..24641fc6 100644
--- a/addons/addon-fit/test/FitAddon.api.ts
+++ b/addons/addon-fit/test/FitAddon.api.ts
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
-import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
+import { openTerminal, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
@@ -75,7 +75,15 @@ describe('FitAddon', () => {
await page.evaluate(`window.term = new Terminal()`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
await loadFit();
- assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
+ const dimensions: { cols: number, rows: number } | undefined = await page.evaluate(`window.fit.proposeDimensions()`);
+ // The value of dims will be undefined if the char measure strategy falls back to the DOM
+ // method, so only assert if it's not undefined.
+ if (dimensions) {
+ assert.isAbove(dimensions.cols, 85);
+ assert.isBelow(dimensions.cols, 88);
+ assert.isAbove(dimensions.rows, 24);
+ assert.isBelow(dimensions.rows, 29);
+ }
});
});
diff --git a/addons/addon-fit/webpack.config.js b/addons/addon-fit/webpack.config.js
index e220668c..aebb523a 100644
--- a/addons/addon-fit/webpack.config.js
+++ b/addons/addon-fit/webpack.config.js
@@ -25,7 +25,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-image/package.json b/addons/addon-image/package.json
index 8572e330..ac4dc018 100644
--- a/addons/addon-image/package.json
+++ b/addons/addon-image/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-image",
- "version": "0.6.0",
+ "version": "0.8.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-image/webpack.config.js b/addons/addon-image/webpack.config.js
index b4283b66..239ebd24 100644
--- a/addons/addon-image/webpack.config.js
+++ b/addons/addon-image/webpack.config.js
@@ -33,7 +33,9 @@ const addon = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-ligatures/package.json b/addons/addon-ligatures/package.json
index 30251888..80608ee3 100644
--- a/addons/addon-ligatures/package.json
+++ b/addons/addon-ligatures/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-ligatures",
- "version": "0.7.0",
+ "version": "0.9.0",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
@@ -36,7 +36,7 @@
},
"devDependencies": {
"@types/sinon": "^5.0.1",
- "axios": "^0.21.2",
+ "axios": "^1.6.0",
"mkdirp": "0.5.5",
"sinon": "6.3.5",
"yauzl": "^2.10.0"
diff --git a/addons/addon-ligatures/webpack.config.js b/addons/addon-ligatures/webpack.config.js
index 6ec7f42d..f9e9f347 100644
--- a/addons/addon-ligatures/webpack.config.js
+++ b/addons/addon-ligatures/webpack.config.js
@@ -25,7 +25,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production',
externals: {
diff --git a/addons/addon-ligatures/yarn.lock b/addons/addon-ligatures/yarn.lock
index 6ba3eccb..ac58cbe2 100644
--- a/addons/addon-ligatures/yarn.lock
+++ b/addons/addon-ligatures/yarn.lock
@@ -45,17 +45,36 @@ array-from@^2.1.1:
resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
-axios@^0.21.2:
- version "0.21.2"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
- integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
+ integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
dependencies:
- follow-redirects "^1.14.0"
+ follow-redirects "^1.15.0"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
diff@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -66,10 +85,10 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
-follow-redirects@^1.14.0:
- version "1.14.8"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
- integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
+follow-redirects@^1.15.0:
+ version "1.15.6"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+ integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
font-finder@^1.0.3:
version "1.0.4"
@@ -95,6 +114,15 @@ font-ligatures@^1.4.1:
lru-cache "^6.0.0"
opentype.js "^0.8.0"
+form-data@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+ integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
get-system-fonts@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/get-system-fonts/-/get-system-fonts-2.0.0.tgz#a43b9a33f05c0715a60176d2aad5ce6e98f0a3c6"
@@ -140,6 +168,18 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
minimist@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
@@ -183,6 +223,11 @@ promise-stream-reader@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649"
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
sinon@6.3.5:
version "6.3.5"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0"
diff --git a/addons/addon-search/package.json b/addons/addon-search/package.json
index 369d11c3..9292a0c4 100644
--- a/addons/addon-search/package.json
+++ b/addons/addon-search/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-search",
- "version": "0.13.0",
+ "version": "0.15.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts
index 3fae7373..d42f4d27 100644
--- a/addons/addon-search/src/SearchAddon.ts
+++ b/addons/addon-search/src/SearchAddon.ts
@@ -6,7 +6,7 @@
import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm';
import type { SearchAddon as ISearchApi } from '@xterm/addon-search';
import { EventEmitter } from 'common/EventEmitter';
-import { Disposable, toDisposable, disposeArray, MutableDisposable } from 'common/Lifecycle';
+import { Disposable, toDisposable, disposeArray, MutableDisposable, getDisposeArrayDisposable } from 'common/Lifecycle';
export interface ISearchOptions {
regex?: boolean;
@@ -78,8 +78,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
*/
private _linesCache: LineCacheEntry[] | undefined;
private _linesCacheTimeoutId = 0;
- private _cursorMoveListener: IDisposable | undefined;
- private _resizeListener: IDisposable | undefined;
+ private _linesCacheDisposables = new MutableDisposable();
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
public readonly onDidChangeResults = this._onDidChangeResults.event;
@@ -427,8 +426,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
const terminal = this._terminal!;
if (!this._linesCache) {
this._linesCache = new Array(terminal.buffer.active.length);
- this._cursorMoveListener = terminal.onCursorMove(() => this._destroyLinesCache());
- this._resizeListener = terminal.onResize(() => this._destroyLinesCache());
+ this._linesCacheDisposables.value = getDisposeArrayDisposable([
+ terminal.onLineFeed(() => this._destroyLinesCache()),
+ terminal.onCursorMove(() => this._destroyLinesCache()),
+ terminal.onResize(() => this._destroyLinesCache())
+ ]);
}
window.clearTimeout(this._linesCacheTimeoutId);
@@ -437,14 +439,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
private _destroyLinesCache(): void {
this._linesCache = undefined;
- if (this._cursorMoveListener) {
- this._cursorMoveListener.dispose();
- this._cursorMoveListener = undefined;
- }
- if (this._resizeListener) {
- this._resizeListener.dispose();
- this._resizeListener = undefined;
- }
+ this._linesCacheDisposables.clear();
if (this._linesCacheTimeoutId) {
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = 0;
diff --git a/addons/addon-search/webpack.config.js b/addons/addon-search/webpack.config.js
index a770f93f..78580548 100644
--- a/addons/addon-search/webpack.config.js
+++ b/addons/addon-search/webpack.config.js
@@ -32,7 +32,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-serialize/package.json b/addons/addon-serialize/package.json
index 763c52ca..30dadbb6 100644
--- a/addons/addon-serialize/package.json
+++ b/addons/addon-serialize/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-serialize",
- "version": "0.11.0",
+ "version": "0.13.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-serialize/src/SerializeAddon.test.ts b/addons/addon-serialize/src/SerializeAddon.test.ts
index d41cfa12..7cb071bf 100644
--- a/addons/addon-serialize/src/SerializeAddon.test.ts
+++ b/addons/addon-serialize/src/SerializeAddon.test.ts
@@ -9,7 +9,6 @@ import { SerializeAddon } from './SerializeAddon';
import { Terminal } from 'browser/public/Terminal';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferService } from 'common/services/Services';
-import { OptionsService } from 'common/services/OptionsService';
import { ThemeService } from 'browser/services/ThemeService';
function sgr(...seq: string[]): string {
@@ -83,6 +82,36 @@ describe('SerializeAddon', () => {
await writeP(terminal, sgr('32') + '> ' + sgr('0'));
assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m');
});
+
+ describe('ISerializeOptions.range', () => {
+ it('should serialize the top line', async () => {
+ await writeP(terminal, 'hello\r\nworld');
+ assert.equal(serializeAddon.serialize({
+ range: {
+ start: 0,
+ end: 0
+ }
+ }), 'hello');
+ });
+ it('should serialize multiple lines from the top', async () => {
+ await writeP(terminal, 'hello\r\nworld');
+ assert.equal(serializeAddon.serialize({
+ range: {
+ start: 0,
+ end: 1
+ }
+ }), 'hello\r\nworld');
+ });
+ it('should serialize lines in the middle', async () => {
+ await writeP(terminal, 'hello\r\nworld');
+ assert.equal(serializeAddon.serialize({
+ range: {
+ start: 1,
+ end: 1
+ }
+ }), 'world');
+ });
+ });
});
describe('html', () => {
@@ -109,6 +138,16 @@ describe('SerializeAddon', () => {
assert.equal((output.match(/terminal<\/span><\/div>/g) || []).length, 1, output);
});
+ it('basic terminal with html unsafe chars', async () => {
+ await writeP(terminal, ' π ');
+ terminal.select(1, 0, 7);
+
+ const output = serializeAddon.serializeAsHTML({
+ onlySelection: true
+ });
+ assert.equal((output.match(/<a>π<\/span><\/div>/g) || []).length, 1, output);
+ });
+
it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts
index 961d8546..cd15cfc3 100644
--- a/addons/addon-serialize/src/SerializeAddon.ts
+++ b/addons/addon-serialize/src/SerializeAddon.ts
@@ -6,7 +6,7 @@
*/
import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
-import type { SerializeAddon as ISerializeApi } from '@xterm/addon-serialize';
+import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
import { DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
import { IAttributeData, IColor } from 'common/Types';
@@ -14,6 +14,14 @@ function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
}
+function escapeHTMLChar(c: string): string {
+ switch (c) {
+ case '&': return '&';
+ case '<': return '<';
+ }
+ return c;
+}
+
// TODO: Refine this template class later
abstract class BaseSerializeHandler {
constructor(
@@ -21,24 +29,24 @@ abstract class BaseSerializeHandler {
) {
}
- public serialize(range: IBufferRange): string {
+ public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
// we need two of them to flip between old and new cell
const cell1 = this._buffer.getNullCell();
const cell2 = this._buffer.getNullCell();
let oldCell = cell1;
- const startRow = range.start.x;
- const endRow = range.end.x;
- const startColumn = range.start.y;
- const endColumn = range.end.y;
+ const startRow = range.start.y;
+ const endRow = range.end.y;
+ const startColumn = range.start.x;
+ const endColumn = range.end.x;
this._beforeSerialize(endRow - startRow, startRow, endRow);
for (let row = startRow; row <= endRow; row++) {
const line = this._buffer.getLine(row);
if (line) {
- const startLineColumn = row !== range.start.x ? 0 : startColumn;
- const endLineColumn = row !== range.end.x ? line.length : endColumn;
+ const startLineColumn = row === range.start.y ? startColumn : 0;
+ const endLineColumn = row === range.end.y ? endColumn: line.length;
for (let col = startLineColumn; col < endLineColumn; col++) {
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
if (!c) {
@@ -54,14 +62,14 @@ abstract class BaseSerializeHandler {
this._afterSerialize();
- return this._serializeString();
+ return this._serializeString(excludeFinalCursorPosition);
}
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
protected _rowEnd(row: number, isLastRow: boolean): void { }
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
protected _afterSerialize(): void { }
- protected _serializeString(): string { return ''; }
+ protected _serializeString(excludeFinalCursorPosition?: boolean): string { return ''; }
}
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
@@ -353,7 +361,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
}
- protected _serializeString(): string {
+ protected _serializeString(excludeFinalCursorPosition: boolean): string {
let rowEnd = this._allRows.length;
// the fixup is only required for data without scrollback
@@ -374,29 +382,31 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
// restore the cursor
- const realCursorRow = this._buffer.baseY + this._buffer.cursorY;
- const realCursorCol = this._buffer.cursorX;
+ if (!excludeFinalCursorPosition) {
+ const realCursorRow = this._buffer.baseY + this._buffer.cursorY;
+ const realCursorCol = this._buffer.cursorX;
- const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
+ const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
- const moveRight = (offset: number): void => {
- if (offset > 0) {
- content += `\u001b[${offset}C`;
- } else if (offset < 0) {
- content += `\u001b[${-offset}D`;
+ const moveRight = (offset: number): void => {
+ if (offset > 0) {
+ content += `\u001b[${offset}C`;
+ } else if (offset < 0) {
+ content += `\u001b[${-offset}D`;
+ }
+ };
+ const moveDown = (offset: number): void => {
+ if (offset > 0) {
+ content += `\u001b[${offset}B`;
+ } else if (offset < 0) {
+ content += `\u001b[${-offset}A`;
+ }
+ };
+
+ if (cursorMoved) {
+ moveDown(realCursorRow - this._lastCursorRow);
+ moveRight(realCursorCol - this._lastCursorCol);
}
- };
- const moveDown = (offset: number): void => {
- if (offset > 0) {
- content += `\u001b[${offset}B`;
- } else if (offset < 0) {
- content += `\u001b[${-offset}A`;
- }
- };
-
- if (cursorMoved) {
- moveDown(realCursorRow - this._lastCursorRow);
- moveRight(realCursorCol - this._lastCursorCol);
}
// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
@@ -419,14 +429,21 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
this._terminal = terminal;
}
- private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
+ private _serializeBufferByScrollback(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
const maxRows = buffer.length;
- const handler = new StringSerializeHandler(buffer, terminal);
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
+ return this._serializeBufferByRange(terminal, buffer, {
+ start: maxRows - correctRows,
+ end: maxRows - 1
+ }, false);
+ }
+
+ private _serializeBufferByRange(terminal: Terminal, buffer: IBuffer, range: ISerializeRange, excludeFinalCursorPosition: boolean): string {
+ const handler = new StringSerializeHandler(buffer, terminal);
return handler.serialize({
- start: { x: maxRows - correctRows, y: 0 },
- end: { x: maxRows - 1, y: terminal.cols }
- });
+ start: { x: 0, y: typeof range.start === 'number' ? range.start : range.start.line },
+ end: { x: terminal.cols, y: typeof range.end === 'number' ? range.end : range.end.line }
+ }, excludeFinalCursorPosition);
}
private _serializeBufferAsHTML(terminal: Terminal, options: Partial): string {
@@ -438,16 +455,16 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
const scrollback = options.scrollback;
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
return handler.serialize({
- start: { x: maxRows - correctRows, y: 0 },
- end: { x: maxRows - 1, y: terminal.cols }
+ start: { x: 0, y: maxRows - correctRows },
+ end: { x: terminal.cols, y: maxRows - 1 }
});
}
const selection = this._terminal?.getSelectionPosition();
if (selection !== undefined) {
return handler.serialize({
- start: { x: selection.start.y, y: selection.start.x },
- end: { x: selection.end.y, y: selection.end.x }
+ start: { x: selection.start.x, y: selection.start.y },
+ end: { x: selection.end.x, y: selection.end.y }
});
}
@@ -490,12 +507,14 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
}
// Normal buffer
- let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback);
+ let content = options?.range
+ ? this._serializeBufferByRange(this._terminal, this._terminal.buffer.normal, options.range, true)
+ : this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.normal, options?.scrollback);
// Alternate buffer
if (!options?.excludeAltBuffer) {
if (this._terminal.buffer.active.type === 'alternate') {
- const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined);
+ const alternativeScreenContent = this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.alternate, undefined);
content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`;
}
}
@@ -519,19 +538,6 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
public dispose(): void { }
}
-
-interface ISerializeOptions {
- scrollback?: number;
- excludeModes?: boolean;
- excludeAltBuffer?: boolean;
-}
-
-interface IHTMLSerializeOptions {
- scrollback: number;
- onlySelection: boolean;
- includeGlobalBackground: boolean;
-}
-
export class HTMLSerializeHandler extends BaseSerializeHandler {
private _currentRow: string = '';
@@ -671,7 +677,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
if (isEmptyCell) {
this._currentRow += ' ';
} else {
- this._currentRow += cell.getChars();
+ this._currentRow += escapeHTMLChar(cell.getChars());
}
}
diff --git a/addons/addon-serialize/typings/addon-serialize.d.ts b/addons/addon-serialize/typings/addon-serialize.d.ts
index 0b127b50..90b8b428 100644
--- a/addons/addon-serialize/typings/addon-serialize.d.ts
+++ b/addons/addon-serialize/typings/addon-serialize.d.ts
@@ -3,7 +3,7 @@
* @license MIT
*/
-import { Terminal, ITerminalAddon } from '@xterm/xterm';
+import { Terminal, ITerminalAddon, IMarker, IBufferRange } from '@xterm/xterm';
declare module '@xterm/addon-serialize' {
/**
@@ -48,10 +48,16 @@ declare module '@xterm/addon-serialize' {
}
export interface ISerializeOptions {
+ /**
+ * The row range to serialize. The an explicit range is specified, the cursor will get its final
+ * repositioning.
+ */
+ range?: ISerializeRange;
+
/**
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
- * serialized.
+ * serialized. This will be ignored if {@link range} is specified.
*/
scrollback?: number;
@@ -85,4 +91,15 @@ declare module '@xterm/addon-serialize' {
*/
includeGlobalBackground: boolean;
}
+
+ export interface ISerializeRange {
+ /**
+ * The line to start serializing (inclusive).
+ */
+ start: IMarker | number;
+ /**
+ * The line to end serializing (inclusive).
+ */
+ end: IMarker | number;
+ }
}
diff --git a/addons/addon-serialize/webpack.config.js b/addons/addon-serialize/webpack.config.js
index bd08ca37..837a73a3 100644
--- a/addons/addon-serialize/webpack.config.js
+++ b/addons/addon-serialize/webpack.config.js
@@ -34,7 +34,8 @@ module.exports = {
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd',
- globalObject: 'this'
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-unicode-graphemes/package.json b/addons/addon-unicode-graphemes/package.json
index d49eda87..3d846286 100644
--- a/addons/addon-unicode-graphemes/package.json
+++ b/addons/addon-unicode-graphemes/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-unicode-graphemes",
- "version": "0.1.0",
+ "version": "0.3.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-unicode-graphemes/webpack.config.js b/addons/addon-unicode-graphemes/webpack.config.js
index 6a80bdea..1ebaecaa 100644
--- a/addons/addon-unicode-graphemes/webpack.config.js
+++ b/addons/addon-unicode-graphemes/webpack.config.js
@@ -32,7 +32,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-unicode11/package.json b/addons/addon-unicode11/package.json
index ad6a4892..9f2c21b8 100644
--- a/addons/addon-unicode11/package.json
+++ b/addons/addon-unicode11/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-unicode11",
- "version": "0.6.0",
+ "version": "0.8.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-unicode11/webpack.config.js b/addons/addon-unicode11/webpack.config.js
index 1913481d..746d2581 100644
--- a/addons/addon-unicode11/webpack.config.js
+++ b/addons/addon-unicode11/webpack.config.js
@@ -33,7 +33,8 @@ module.exports = {
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd',
- globalObject: 'this'
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-web-links/package.json b/addons/addon-web-links/package.json
index 6367907a..da888716 100644
--- a/addons/addon-web-links/package.json
+++ b/addons/addon-web-links/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-web-links",
- "version": "0.9.0",
+ "version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-web-links/src/WebLinkProvider.ts b/addons/addon-web-links/src/WebLinkProvider.ts
index 25dd983c..66691f44 100644
--- a/addons/addon-web-links/src/WebLinkProvider.ts
+++ b/addons/addon-web-links/src/WebLinkProvider.ts
@@ -41,6 +41,20 @@ export class WebLinkProvider implements ILinkProvider {
}
}
+function isUrl(urlString: string): boolean {
+ try {
+ const url = new URL(urlString);
+ const parsedBase = url.password && url.username
+ ? `${url.protocol}//${url.username}:${url.password}@${url.host}`
+ : url.username
+ ? `${url.protocol}//${url.username}@${url.host}`
+ : `${url.protocol}//${url.host}`;
+ return urlString.toLocaleLowerCase().startsWith(parsedBase.toLocaleLowerCase());
+ } catch (e) {
+ return false;
+ }
+}
+
export class LinkComputer {
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
@@ -55,20 +69,7 @@ export class LinkComputer {
const text = match[0];
// check via URL if the matched text would form a proper url
- // NOTE: This outsources the ugly url parsing to the browser.
- // To avoid surprising auto expansion from URL we additionally
- // check afterwards if the provided string resembles the parsed
- // one close enough:
- // - decodeURI decode path segement back to byte repr
- // to detect unicode auto conversion correctly
- // - append / also match domain urls w'o any path notion
- try {
- const url = new URL(text);
- const urlText = decodeURI(url.toString());
- if (text !== urlText && text + '/' !== urlText) {
- continue;
- }
- } catch (e) {
+ if (!isUrl(text)) {
continue;
}
diff --git a/addons/addon-web-links/src/WebLinksAddon.ts b/addons/addon-web-links/src/WebLinksAddon.ts
index 8902d8e0..b3f0548c 100644
--- a/addons/addon-web-links/src/WebLinksAddon.ts
+++ b/addons/addon-web-links/src/WebLinksAddon.ts
@@ -18,7 +18,7 @@ import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
// - final interpunction like ,.!?
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
// - unsafe chars from rfc1738: {}|\^~[]`
-const strictUrlRegex = /https?:[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;
+const strictUrlRegex = /(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;
function handleLink(event: MouseEvent, uri: string): void {
diff --git a/addons/addon-web-links/test/WebLinksAddon.api.ts b/addons/addon-web-links/test/WebLinksAddon.api.ts
index fb5be20b..99c11600 100644
--- a/addons/addon-web-links/test/WebLinksAddon.api.ts
+++ b/addons/addon-web-links/test/WebLinksAddon.api.ts
@@ -115,6 +115,28 @@ describe('WebLinksAddon', () => {
await resetAndHover(5, 1);
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
});
+ it('url encoded params work properly', async () => {
+ await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3');
+ await resetAndHover(12, 0);
+ await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } });
+ await resetAndHover(5, 1);
+ await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } });
+ });
+ });
+
+ // issue #4964
+ it('uppercase in protocol and host, default ports', async () => {
+ const data = ` HTTP://EXAMPLE.COM \\r\\n` +
+ ` HTTPS://Example.com \\r\\n` +
+ ` HTTP://Example.com:80 \\r\\n` +
+ ` HTTP://Example.com:80/staysUpper \\r\\n` +
+ ` HTTP://Ab:xY@abc.com:80/staysUpper \\r\\n`;
+ await writeSync(page, data);
+ await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`);
+ await pollForLinkAtCell(3, 1, `HTTPS://Example.com`);
+ await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`);
+ await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`);
+ await pollForLinkAtCell(3, 4, `HTTP://Ab:xY@abc.com:80/staysUpper`);
});
});
diff --git a/addons/addon-web-links/webpack.config.js b/addons/addon-web-links/webpack.config.js
index 4484dbf6..e8dcecef 100644
--- a/addons/addon-web-links/webpack.config.js
+++ b/addons/addon-web-links/webpack.config.js
@@ -25,7 +25,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/addons/addon-webgl/package.json b/addons/addon-webgl/package.json
index 9a31c306..da37eb26 100644
--- a/addons/addon-webgl/package.json
+++ b/addons/addon-webgl/package.json
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-webgl",
- "version": "0.16.0",
+ "version": "0.18.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts
index 1fb0e18c..35b56eef 100644
--- a/addons/addon-webgl/src/GlyphRenderer.ts
+++ b/addons/addon-webgl/src/GlyphRenderer.ts
@@ -3,7 +3,7 @@
* @license MIT
*/
-import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
+import { allowRescaling, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
@@ -11,6 +11,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
import { Terminal } from '@xterm/xterm';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
+import type { IOptionsService } from 'common/services/Services';
interface IVertices {
attributes: Float32Array;
@@ -111,7 +112,8 @@ export class GlyphRenderer extends Disposable {
constructor(
private readonly _terminal: Terminal,
private readonly _gl: IWebGL2RenderingContext,
- private _dimensions: IRenderDimensions
+ private _dimensions: IRenderDimensions,
+ private readonly _optionsService: IOptionsService
) {
super();
@@ -212,15 +214,15 @@ export class GlyphRenderer extends Disposable {
return this._atlas ? this._atlas.beginFrame() : true;
}
- public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
+ public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
// should not instantiate any variables unless a new glyph is drawn to the cache where the
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
// each glyph.
- this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
+ this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, width, lastBg);
}
- private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
+ private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
$i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
// Exit early if this is a null character, allow space character to continue as it may have
@@ -275,6 +277,14 @@ export class GlyphRenderer extends Disposable {
array[$i + 8] = $glyph.sizeClipSpace.y;
}
// a_cellpos only changes on resize
+
+ // Reduce scale horizontally for wide glyphs printed in cells that would overlap with the
+ // following cell (ie. the width is not 2).
+ if (this._optionsService.rawOptions.rescaleOverlappingGlyphs) {
+ if (allowRescaling(code, width, $glyph.size.x, this._dimensions.device.cell.width)) {
+ array[$i + 2] = (this._dimensions.device.cell.width - 1) / this._dimensions.device.canvas.width; // - 1 to improve readability
+ }
+ }
}
public clear(): void {
diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts
index 2bccc9b7..fa178652 100644
--- a/addons/addon-webgl/src/WebglRenderer.ts
+++ b/addons/addon-webgl/src/WebglRenderer.ts
@@ -33,9 +33,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _charAtlasDisposable = this.register(new MutableDisposable());
private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
+ private _observerDisposable = this.register(new MutableDisposable());
private _model: RenderModel = new RenderModel();
- private _workCell: CellData = new CellData();
+ private _workCell: ICellData = new CellData();
+ private _workCell2: ICellData = new CellData();
private _cellColorResolver: CellColorResolver;
private _canvas: HTMLCanvasElement;
@@ -80,7 +82,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core = (this._terminal as any)._core;
this._renderLayers = [
- new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService)
+ new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier!, this._coreBrowserService, _optionsService, this._themeService)
];
this.dimensions = createRenderDimensions();
this._devicePixelRatio = this._coreBrowserService.dpr;
@@ -123,7 +125,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._requestRedrawViewport();
}));
- this.register(observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
+ this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
+ this.register(this._coreBrowserService.onWindowChange(w => {
+ this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
+ }));
this._core.screenElement!.appendChild(this._canvas);
@@ -241,7 +246,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
*/
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService);
- this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions);
+ this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions, this._optionsService);
// Update dimensions and acquire char atlas
this.handleCharSizeChanged();
@@ -384,6 +389,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
let range: [number, number];
let chars: string;
let code: number;
+ let width: number;
let i: number;
let x: number;
let j: number;
@@ -496,7 +502,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
- this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
+ width = cell.getWidth();
+ this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, width, lastBg);
if (isJoined) {
// Restore work cell
@@ -505,7 +512,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Null out non-first cells
for (x++; x < lastCharX; x++) {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
- this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
+ this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
diff --git a/addons/addon-webgl/test/WebglRenderer.test.ts b/addons/addon-webgl/test/WebglRenderer.test.ts
index 4b73c08b..d5f62fda 100644
--- a/addons/addon-webgl/test/WebglRenderer.test.ts
+++ b/addons/addon-webgl/test/WebglRenderer.test.ts
@@ -29,5 +29,10 @@ test.describe('WebGL Renderer Integration Tests', async () => {
}
injectSharedRendererTests(ctxWrapper);
- injectSharedRendererTestsStandalone(ctxWrapper);
+ injectSharedRendererTestsStandalone(ctxWrapper, async () => {
+ await ctx.page.evaluate(`
+ window.addon = new window.WebglAddon(true);
+ window.term.loadAddon(window.addon);
+ `);
+ });
});
diff --git a/addons/addon-webgl/webpack.config.js b/addons/addon-webgl/webpack.config.js
index f31ffd51..7365acff 100644
--- a/addons/addon-webgl/webpack.config.js
+++ b/addons/addon-webgl/webpack.config.js
@@ -33,7 +33,9 @@ module.exports = {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
- libraryTarget: 'umd'
+ libraryTarget: 'umd',
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
mode: 'production'
};
diff --git a/bin/publish.js b/bin/publish.js
index 360e09fe..4b945d75 100644
--- a/bin/publish.js
+++ b/bin/publish.js
@@ -29,6 +29,7 @@ if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
const addonPackageDirs = [
path.resolve(__dirname, '../addons/addon-attach'),
path.resolve(__dirname, '../addons/addon-canvas'),
+ path.resolve(__dirname, '../addons/addon-clipboard'),
path.resolve(__dirname, '../addons/addon-fit'),
path.resolve(__dirname, '../addons/addon-image'),
path.resolve(__dirname, '../addons/addon-ligatures'),
@@ -57,9 +58,8 @@ function checkAndPublishPackage(packageDir) {
const packageJson = require(path.join(packageDir, 'package.json'));
// Determine if this is a stable or beta release
- // TODO: Uncomment when publishing 5.4
- // const publishedVersions = getPublishedVersions(packageJson);
- const isStableRelease = false; //!publishedVersions.includes(packageJson.version);
+ const publishedVersions = getPublishedVersions(packageJson);
+ const isStableRelease = !publishedVersions.includes(packageJson.version);
// Get the next version
let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(packageJson);
diff --git a/demo/client.ts b/demo/client.ts
index 7e0830a5..78f8396d 100644
--- a/demo/client.ts
+++ b/demo/client.ts
@@ -12,6 +12,7 @@
import { Terminal } from '../out/browser/public/Terminal';
import { AttachAddon } from '../addons/addon-attach/out/AttachAddon';
import { CanvasAddon } from '../addons/addon-canvas/out/CanvasAddon';
+import { ClipboardAddon } from '../addons/addon-clipboard/out/ClipboardAddon';
import { FitAddon } from '../addons/addon-fit/out/FitAddon';
import { SearchAddon, ISearchOptions } from '../addons/addon-search/out/SearchAddon';
import { SerializeAddon } from '../addons/addon-serialize/out/SerializeAddon';
@@ -32,6 +33,7 @@ if ('WebAssembly' in window) {
// Use webpacked version (yarn package)
// import { Terminal } from '../lib/xterm';
// import { AttachAddon } from '@xterm/addon-attach';
+// import { ClipboardAddon } from '@xterm/addon-clipboard';
// import { FitAddon } from '@xterm/addon-fit';
// import { ImageAddon } from '@xterm/addon-image';
// import { SearchAddon, ISearchOptions } from '@xterm/addon-search';
@@ -44,13 +46,14 @@ if ('WebAssembly' in window) {
// Pulling in the module's types relies on the above, it's looks a
// little weird here as we're importing "this" module
-import { Terminal as TerminalType, ITerminalOptions } from '@xterm/xterm';
+import { Terminal as TerminalType, ITerminalOptions, type IDisposable } from '@xterm/xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
Terminal?: typeof TerminalType; // eslint-disable-line @typescript-eslint/naming-convention
AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention
CanvasAddon?: typeof CanvasAddon; // eslint-disable-line @typescript-eslint/naming-convention
+ ClipboardAddon?: typeof ClipboardAddon; // eslint-disable-line @typescript-eslint/naming-convention
FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention
ImageAddon?: typeof ImageAddonType; // eslint-disable-line @typescript-eslint/naming-convention
SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention
@@ -70,7 +73,7 @@ let socket;
let pid;
let autoResize: boolean = true;
-type AddonType = 'attach' | 'canvas' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures';
+type AddonType = 'attach' | 'canvas' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures';
interface IDemoAddon {
name: T;
@@ -78,35 +81,38 @@ interface IDemoAddon {
ctor: (
T extends 'attach' ? typeof AttachAddon :
T extends 'canvas' ? typeof CanvasAddon :
- T extends 'fit' ? typeof FitAddon :
- T extends 'image' ? typeof ImageAddonType :
- T extends 'search' ? typeof SearchAddon :
- T extends 'serialize' ? typeof SerializeAddon :
- T extends 'webLinks' ? typeof WebLinksAddon :
- T extends 'unicode11' ? typeof Unicode11Addon :
- T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon :
- T extends 'ligatures' ? typeof LigaturesAddon :
+ T extends 'clipboard' ? typeof ClipboardAddon :
+ T extends 'fit' ? typeof FitAddon :
+ T extends 'image' ? typeof ImageAddonType :
+ T extends 'search' ? typeof SearchAddon :
+ T extends 'serialize' ? typeof SerializeAddon :
+ T extends 'webLinks' ? typeof WebLinksAddon :
+ T extends 'unicode11' ? typeof Unicode11Addon :
+ T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon :
+ T extends 'ligatures' ? typeof LigaturesAddon :
typeof WebglAddon
);
instance?: (
T extends 'attach' ? AttachAddon :
T extends 'canvas' ? CanvasAddon :
- T extends 'fit' ? FitAddon :
- T extends 'image' ? ImageAddonType :
- T extends 'search' ? SearchAddon :
- T extends 'serialize' ? SerializeAddon :
- T extends 'webLinks' ? WebLinksAddon :
- T extends 'webgl' ? WebglAddon :
- T extends 'unicode11' ? typeof Unicode11Addon :
- T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon :
- T extends 'ligatures' ? typeof LigaturesAddon :
- never
+ T extends 'clipboard' ? ClipboardAddon :
+ T extends 'fit' ? FitAddon :
+ T extends 'image' ? ImageAddonType :
+ T extends 'search' ? SearchAddon :
+ T extends 'serialize' ? SerializeAddon :
+ T extends 'webLinks' ? WebLinksAddon :
+ T extends 'webgl' ? WebglAddon :
+ T extends 'unicode11' ? typeof Unicode11Addon :
+ T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon :
+ T extends 'ligatures' ? typeof LigaturesAddon :
+ never
);
}
const addons: { [T in AddonType]: IDemoAddon } = {
attach: { name: 'attach', ctor: AttachAddon, canChange: false },
canvas: { name: 'canvas', ctor: CanvasAddon, canChange: true },
+ clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true },
fit: { name: 'fit', ctor: FitAddon, canChange: false },
image: { name: 'image', ctor: ImageAddon, canChange: true },
search: { name: 'search', ctor: SearchAddon, canChange: true },
@@ -179,6 +185,7 @@ const disposeRecreateButtonHandler: () => void = () => {
socket = null;
addons.attach.instance = undefined;
addons.canvas.instance = undefined;
+ addons.clipboard.instance = undefined;
addons.fit.instance = undefined;
addons.image.instance = undefined;
addons.search.instance = undefined;
@@ -228,6 +235,7 @@ if (document.location.pathname === '/test') {
window.Terminal = Terminal;
window.AttachAddon = AttachAddon;
window.CanvasAddon = CanvasAddon;
+ window.ClipboardAddon = ClipboardAddon;
window.FitAddon = FitAddon;
window.ImageAddon = ImageAddon;
window.SearchAddon = SearchAddon;
@@ -255,6 +263,7 @@ if (document.location.pathname === '/test') {
document.getElementById('add-grapheme-clusters').addEventListener('click', addGraphemeClusters);
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
+ document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest);
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
document.getElementById('bce').addEventListener('click', coloredErase);
addVtButtons();
@@ -287,6 +296,7 @@ function createTerminal(): void {
addons.fit.instance = new FitAddon();
addons.image.instance = new ImageAddon();
addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon();
+ addons.clipboard.instance = new ClipboardAddon();
try { // try to start with webgl renderer (might throw on older safari/webkit)
addons.webgl.instance = new WebglAddon();
} catch (e) {
@@ -299,6 +309,7 @@ function createTerminal(): void {
typedTerm.loadAddon(addons.serialize.instance);
typedTerm.loadAddon(addons.unicodeGraphemes.instance);
typedTerm.loadAddon(addons.webLinks.instance);
+ typedTerm.loadAddon(addons.clipboard.instance);
window.term = term; // Expose `term` to window for debugging purposes
term.onResize((size: { cols: number, rows: number }) => {
@@ -1170,6 +1181,33 @@ function addOverviewRuler(): void {
term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } });
}
+let decorationStressTestDecorations: IDisposable[] | undefined;
+function decorationStressTest(): void {
+ if (decorationStressTestDecorations) {
+ for (const d of decorationStressTestDecorations) {
+ d.dispose();
+ }
+ decorationStressTestDecorations = undefined;
+ } else {
+ const t = term as Terminal;
+ const buffer = t.buffer.active;
+ const cursorY = buffer.baseY + buffer.cursorY;
+ decorationStressTestDecorations = [];
+ for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) {
+ for (let y = 0; y < t.buffer.active.length; y++) {
+ const cursorOffsetY = y - cursorY;
+ decorationStressTestDecorations.push(t.registerDecoration({
+ marker: t.registerMarker(cursorOffsetY),
+ x,
+ width: 4,
+ backgroundColor: '#FF0000',
+ overviewRulerOptions: { color: '#FF0000' }
+ }));
+ }
+ }
+ }
+}
+
(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
function getBox(width: number, height: number): any {
return {
diff --git a/demo/index.html b/demo/index.html
index caff2ca2..238c9886 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -102,6 +102,7 @@
Decorations
+
Weblinks Addon
diff --git a/demo/tsconfig.json b/demo/tsconfig.json
index f0aebb1f..4114f6d6 100644
--- a/demo/tsconfig.json
+++ b/demo/tsconfig.json
@@ -7,6 +7,7 @@
"baseUrl": ".",
"paths": {
"addon-attach": ["../addons/addon-attach"],
+ "addon-clipboard": ["../addons/addon-clipboard"],
"addon-fit": ["../addons/addon-fit"],
"addon-image": ["../addons/addon-image"],
"addon-search": ["../addons/addon-search"],
diff --git a/package.json b/package.json
index d0510e5d..c79b1196 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@xterm/xterm",
"description": "Full xterm terminal, in your browser",
- "version": "5.3.0",
+ "version": "5.5.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
@@ -78,8 +78,8 @@
"chai": "^4.3.4",
"cross-env": "^7.0.3",
"deep-equal": "^2.0.5",
- "eslint": "^8.45.0",
- "eslint-plugin-jsdoc": "^39.3.6",
+ "eslint": "^8.56.0",
+ "eslint-plugin-jsdoc": "^46.9.1",
"express": "^4.17.1",
"express-ws": "^5.0.2",
"glob": "^7.2.0",
diff --git a/src/browser/Linkifier2.test.ts b/src/browser/Linkifier.test.ts
similarity index 84%
rename from src/browser/Linkifier2.test.ts
rename to src/browser/Linkifier.test.ts
index 0af74c28..4294ef10 100644
--- a/src/browser/Linkifier2.test.ts
+++ b/src/browser/Linkifier.test.ts
@@ -5,11 +5,13 @@
import { assert } from 'chai';
import { IBufferService } from 'common/services/Services';
-import { Linkifier2 } from 'browser/Linkifier2';
+import { Linkifier } from './Linkifier';
import { MockBufferService } from 'common/TestUtils.test';
import { ILink } from 'browser/Types';
+import { LinkProviderService } from 'browser/services/LinkProviderService';
+import jsdom = require('jsdom');
-class TestLinkifier2 extends Linkifier2 {
+class TestLinkifier2 extends Linkifier {
public set currentLink(link: any) {
this._currentLink = link;
}
@@ -43,8 +45,9 @@ describe('Linkifier2', () => {
};
beforeEach(() => {
+ const dom = new jsdom.JSDOM();
bufferService = new MockBufferService(100, 10);
- linkifier = new TestLinkifier2(bufferService);
+ linkifier = new TestLinkifier2(dom.window.document.createElement('div'), null!, null!, bufferService, new LinkProviderService());
linkifier.currentLink = {
link,
state: {
diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier.ts
similarity index 82%
rename from src/browser/Linkifier2.ts
rename to src/browser/Linkifier.ts
index 28002e04..ac37e42f 100644
--- a/src/browser/Linkifier2.ts
+++ b/src/browser/Linkifier.ts
@@ -4,18 +4,14 @@
*/
import { addDisposableDomListener } from 'browser/Lifecycle';
-import { IBufferCellPosition, ILink, ILinkDecorations, ILinkProvider, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
+import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, disposeArray, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { IBufferService } from 'common/services/Services';
-import { IMouseService, IRenderService } from './services/Services';
+import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
-export class Linkifier2 extends Disposable implements ILinkifier2 {
- private _element: HTMLElement | undefined;
- private _mouseService: IMouseService | undefined;
- private _renderService: IRenderService | undefined;
- private _linkProviders: ILinkProvider[] = [];
+export class Linkifier extends Disposable implements ILinkifier2 {
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
protected _currentLink: ILinkWithState | undefined;
private _mouseDownLink: ILinkWithState | undefined;
@@ -33,39 +29,24 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
constructor(
- @IBufferService private readonly _bufferService: IBufferService
+ private readonly _element: HTMLElement,
+ @IMouseService private readonly _mouseService: IMouseService,
+ @IRenderService private readonly _renderService: IRenderService,
+ @IBufferService private readonly _bufferService: IBufferService,
+ @ILinkProviderService private readonly _linkProviderService: ILinkProviderService
) {
super();
this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
this.register(toDisposable(() => {
this._lastMouseEvent = undefined;
+ // Clear out link providers as they could easily cause an embedder memory leak
+ this._activeProviderReplies?.clear();
}));
// Listen to resize to catch the case where it's resized and the cursor is out of the viewport.
this.register(this._bufferService.onResize(() => {
this._clearCurrentLink();
this._wasResized = true;
}));
- }
-
- public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
- this._linkProviders.push(linkProvider);
- return {
- dispose: () => {
- // Remove the link provider from the list
- const providerIndex = this._linkProviders.indexOf(linkProvider);
-
- if (providerIndex !== -1) {
- this._linkProviders.splice(providerIndex, 1);
- }
- }
- };
- }
-
- public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void {
- this._element = element;
- this._mouseService = mouseService;
- this._renderService = renderService;
-
this.register(addDisposableDomListener(this._element, 'mouseleave', () => {
this._isMouseOut = true;
this._clearCurrentLink();
@@ -78,10 +59,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _handleMouseMove(event: MouseEvent): void {
this._lastMouseEvent = event;
- if (!this._element || !this._mouseService) {
- return;
- }
-
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
if (!position) {
return;
@@ -142,7 +119,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
let linkProvided = false;
// There is no link cached, so ask for one
- for (const [i, linkProvider] of this._linkProviders.entries()) {
+ for (const [i, linkProvider] of this._linkProviderService.linkProviders.entries()) {
if (useLineCache) {
const existingReply = this._activeProviderReplies?.get(i);
// If there isn't a reply, the provider hasn't responded yet.
@@ -164,7 +141,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
// If all providers have responded, remove lower priority links that intersect ranges of
// higher priority links
- if (this._activeProviderReplies?.size === this._linkProviders.length) {
+ if (this._activeProviderReplies?.size === this._linkProviderService.linkProviders.length) {
this._removeIntersectingLinks(position.y, this._activeProviderReplies);
}
});
@@ -220,7 +197,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
// Check if all the providers have responded
- if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) {
+ if (this._activeProviderReplies.size === this._linkProviderService.linkProviders.length && !linkProvided) {
// Respect the order of the link providers
for (let j = 0; j < this._activeProviderReplies.size; j++) {
const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position));
@@ -240,7 +217,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _handleMouseUp(event: MouseEvent): void {
- if (!this._element || !this._mouseService || !this._currentLink) {
+ if (!this._currentLink) {
return;
}
@@ -255,7 +232,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _clearCurrentLink(startRow?: number, endRow?: number): void {
- if (!this._element || !this._currentLink || !this._lastMouseEvent) {
+ if (!this._currentLink || !this._lastMouseEvent) {
return;
}
@@ -268,7 +245,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _handleNewLink(linkWithState: ILinkWithState): void {
- if (!this._element || !this._lastMouseEvent || !this._mouseService) {
+ if (!this._lastMouseEvent) {
return;
}
@@ -299,7 +276,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
this._currentLink.state.decorations.pointerCursor = v;
if (this._currentLink.state.isHovered) {
- this._element?.classList.toggle('xterm-cursor-pointer', v);
+ this._element.classList.toggle('xterm-cursor-pointer', v);
}
}
}
@@ -319,29 +296,27 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
// Listen to viewport changes to re-render the link under the cursor (only when the line the
// link is on changes)
- if (this._renderService) {
- this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
- // Sanity check, this shouldn't happen in practice as this listener would be disposed
- if (!this._currentLink) {
- return;
- }
- // When start is 0 a scroll most likely occurred, make sure links above the fold also get
- // cleared.
- const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
- const end = this._bufferService.buffer.ydisp + 1 + e.end;
- // Only clear the link if the viewport change happened on this line
- if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
- this._clearCurrentLink(start, end);
- if (this._lastMouseEvent && this._element) {
- // re-eval previously active link after changes
- const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
- if (position) {
- this._askForLink(position, false);
- }
+ this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
+ // Sanity check, this shouldn't happen in practice as this listener would be disposed
+ if (!this._currentLink) {
+ return;
+ }
+ // When start is 0 a scroll most likely occurred, make sure links above the fold also get
+ // cleared.
+ const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
+ const end = this._bufferService.buffer.ydisp + 1 + e.end;
+ // Only clear the link if the viewport change happened on this line
+ if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
+ this._clearCurrentLink(start, end);
+ if (this._lastMouseEvent) {
+ // re-eval previously active link after changes
+ const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
+ if (position) {
+ this._askForLink(position, false);
}
}
- }));
- }
+ }
+ }));
}
}
diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts
index fee1ae7c..a079fe67 100644
--- a/src/browser/OscLinkProvider.ts
+++ b/src/browser/OscLinkProvider.ts
@@ -3,7 +3,8 @@
* @license MIT
*/
-import { IBufferRange, ILink, ILinkProvider } from 'browser/Types';
+import { IBufferRange, ILink } from 'browser/Types';
+import { ILinkProvider } from 'browser/services/Services';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts
index b3118d5f..dd3b97a6 100644
--- a/src/browser/RenderDebouncer.ts
+++ b/src/browser/RenderDebouncer.ts
@@ -4,6 +4,7 @@
*/
import { IRenderDebouncerWithCallback } from 'browser/Types';
+import { ICoreBrowserService } from 'browser/services/Services';
/**
* Debounces calls to render terminal rows using animation frames.
@@ -16,14 +17,14 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
private _refreshCallbacks: FrameRequestCallback[] = [];
constructor(
- private _parentWindow: Window,
- private _renderCallback: (start: number, end: number) => void
+ private _renderCallback: (start: number, end: number) => void,
+ private readonly _coreBrowserService: ICoreBrowserService
) {
}
public dispose(): void {
if (this._animationFrame) {
- this._parentWindow.cancelAnimationFrame(this._animationFrame);
+ this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
@@ -31,7 +32,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
public addRefreshCallback(callback: FrameRequestCallback): number {
this._refreshCallbacks.push(callback);
if (!this._animationFrame) {
- this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
+ this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
}
return this._animationFrame;
}
@@ -49,7 +50,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
return;
}
- this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
+ this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
}
private _innerRefresh(): void {
diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts
index fad2d80b..0e945aa9 100644
--- a/src/browser/Terminal.ts
+++ b/src/browser/Terminal.ts
@@ -23,10 +23,10 @@
import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard';
import { addDisposableDomListener } from 'browser/Lifecycle';
-import { Linkifier2 } from 'browser/Linkifier2';
+import { Linkifier } from './Linkifier';
import * as Strings from 'browser/LocalizableStrings';
import { OscLinkProvider } from 'browser/OscLinkProvider';
-import { CharacterJoinerHandler, CustomKeyEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
+import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
import { Viewport } from 'browser/Viewport';
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
@@ -39,9 +39,9 @@ import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { MouseService } from 'browser/services/MouseService';
import { RenderService } from 'browser/services/RenderService';
import { SelectionService } from 'browser/services/SelectionService';
-import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
+import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ThemeService } from 'browser/services/ThemeService';
-import { color, rgba } from 'common/Color';
+import { channels, color } from 'common/Color';
import { CoreTerminal } from 'common/CoreTerminal';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
@@ -57,6 +57,7 @@ import { IDecorationService } from 'common/services/Services';
import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker } from '@xterm/xterm';
import { WindowsOptionsReportType } from '../common/InputHandler';
import { AccessibilityManager } from './AccessibilityManager';
+import { LinkProviderService } from 'browser/services/LinkProviderService';
export class Terminal extends CoreTerminal implements ITerminal {
public textarea: HTMLTextAreaElement | undefined;
@@ -69,14 +70,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _helperContainer: HTMLElement | undefined;
private _compositionView: HTMLElement | undefined;
+ public linkifier: ILinkifier2 | undefined;
private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
public browser: IBrowser = Browser as any;
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
+ private _customWheelEventHandler: CustomWheelEventHandler | undefined;
- // browser services
+ // Browser services
private _decorationService: DecorationService;
+ private _linkProviderService: ILinkProviderService;
+
+ // Optional browser services
private _charSizeService: ICharSizeService | undefined;
private _coreBrowserService: ICoreBrowserService | undefined;
private _mouseService: IMouseService | undefined;
@@ -112,7 +118,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
*/
private _unprocessedDeadKey: boolean = false;
- public linkifier2: ILinkifier2;
public viewport: IViewport | undefined;
private _compositionHelper: ICompositionHelper | undefined;
private _accessibilityManager: MutableDisposable = this.register(new MutableDisposable());
@@ -148,10 +153,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._setup();
- this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
- this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
this._decorationService = this._instantiationService.createInstance(DecorationService);
this._instantiationService.setService(IDecorationService, this._decorationService);
+ this._linkProviderService = this._instantiationService.createInstance(LinkProviderService);
+ this._instantiationService.setService(ILinkProviderService, this._linkProviderService);
+ this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
// Setup InputHandler listeners
this.register(this._inputHandler.onRequestBell(() => this._onBell.fire()));
@@ -205,17 +211,17 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
switch (req.type) {
case ColorRequestType.REPORT:
- const channels = color.toColorRGB(acc === 'ansi'
+ const colorRgb = color.toColorRGB(acc === 'ansi'
? this._themeService.colors.ansi[req.index]
: this._themeService.colors[acc]);
- this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
+ this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`);
break;
case ColorRequestType.SET:
if (acc === 'ansi') {
- this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color));
+ this._themeService.modifyColors(colors => colors.ansi[req.index] = channels.toColor(...req.color));
} else {
const narrowedAcc = acc;
- this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color));
+ this._themeService.modifyColors(colors => colors[narrowedAcc] = channels.toColor(...req.color));
}
break;
case ColorRequestType.RESTORE:
@@ -260,11 +266,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
/**
* Binds the desired focus behavior on a given terminal object.
*/
- private _handleTextAreaFocus(ev: KeyboardEvent): void {
+ private _handleTextAreaFocus(ev: FocusEvent): void {
if (this.coreService.decPrivateModes.sendFocus) {
this.coreService.triggerDataEvent(C0.ESC + '[I');
}
- this.updateCursorStyle(ev);
this.element!.classList.add('focus');
this._showCursor();
this._onFocus.fire();
@@ -428,6 +433,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.screenElement = this._document.createElement('div');
this.screenElement.classList.add('xterm-screen');
+ this.register(addDisposableDomListener(this.screenElement, 'mousemove', (ev: MouseEvent) => this.updateCursorStyle(ev)));
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
this._helperContainer = this._document.createElement('div');
@@ -458,11 +464,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
));
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
- this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
+ this.register(addDisposableDomListener(this.textarea, 'focus', (ev: FocusEvent) => this._handleTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
-
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
@@ -482,6 +487,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
this._helperContainer.appendChild(this._compositionView);
+ this._mouseService = this._instantiationService.createInstance(MouseService);
+ this._instantiationService.setService(IMouseService, this._mouseService);
+
+ this.linkifier = this.register(this._instantiationService.createInstance(Linkifier, this.screenElement));
+
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
@@ -493,9 +503,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._renderService.setRenderer(this._createRenderer());
}
- this._mouseService = this._instantiationService.createInstance(MouseService);
- this._instantiationService.setService(IMouseService, this._mouseService);
-
this.viewport = this._instantiationService.createInstance(Viewport, this._viewportElement, this._viewportScrollArea);
this.viewport.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)),
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
@@ -513,7 +520,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
this.element,
this.screenElement,
- this.linkifier2
+ this.linkifier
));
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
@@ -533,7 +540,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));
- this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
@@ -575,7 +581,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _createRenderer(): IRenderer {
- return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier2);
+ return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
}
/**
@@ -633,6 +639,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
break;
case 'wheel':
+ if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
+ return false;
+ }
const amount = self.viewport!.getLinesScrolled(ev as WheelEvent);
if (amount === 0) {
@@ -792,6 +801,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
// do nothing, if app side handles wheel itself
if (requestedEvents.wheel) return;
+ if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
+ return false;
+ }
+
if (!this.buffer.hasScrollback) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
@@ -847,7 +860,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
/**
* Change the cursor style for different selection modes
*/
- public updateCursorStyle(ev: KeyboardEvent): void {
+ public updateCursorStyle(ev: KeyboardEvent | MouseEvent): void {
if (this._selectionService?.shouldColumnSelect(ev)) {
this.element!.classList.add('column-select');
} else {
@@ -878,21 +891,16 @@ export class Terminal extends CoreTerminal implements ITerminal {
paste(data, this.textarea!, this.coreService, this.optionsService);
}
- /**
- * Attaches a custom key event handler which is run before keys are processed,
- * giving consumers of xterm.js ultimate control as to what keys should be
- * processed by the terminal and what keys should not.
- * @param customKeyEventHandler The custom KeyboardEvent handler to attach.
- * This is a function that takes a KeyboardEvent, allowing consumers to stop
- * propagation and/or prevent the default action. The function returns whether
- * the event should be processed by xterm.js.
- */
public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void {
this._customKeyEventHandler = customKeyEventHandler;
}
+ public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
+ this._customWheelEventHandler = customWheelEventHandler;
+ }
+
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
- return this.linkifier2.registerLinkProvider(linkProvider);
+ return this._linkProviderService.registerLinkProvider(linkProvider);
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts
index 7e43017a..c7c8438c 100644
--- a/src/browser/TestUtils.test.ts
+++ b/src/browser/TestUtils.test.ts
@@ -48,6 +48,7 @@ export class MockTerminal implements ITerminal {
public onRender!: IEvent<{ start: number, end: number }>;
public onResize!: IEvent<{ cols: number, rows: number }>;
public markers!: IMarker[];
+ public linkifier: ILinkifier2 | undefined;
public coreMouseService!: ICoreMouseService;
public coreService!: ICoreService;
public optionsService!: IOptionsService;
@@ -71,6 +72,9 @@ export class MockTerminal implements ITerminal {
public focus(): void {
throw new Error('Method not implemented.');
}
+ public input(data: string, wasUserInput: boolean = true): void {
+ throw new Error('Method not implemented.');
+ }
public resize(columns: number, rows: number): void {
throw new Error('Method not implemented.');
}
@@ -86,6 +90,9 @@ export class MockTerminal implements ITerminal {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
+ public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void {
+ throw new Error('Method not implemented.');
+ }
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable {
throw new Error('Method not implemented.');
}
@@ -148,7 +155,6 @@ export class MockTerminal implements ITerminal {
}
public bracketedPasteMode!: boolean;
public renderer!: IRenderer;
- public linkifier2!: ILinkifier2;
public isFocused!: boolean;
public options!: Required;
public element!: HTMLElement;
diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts
index b1f31b20..9ebc55d9 100644
--- a/src/browser/Types.d.ts
+++ b/src/browser/Types.d.ts
@@ -7,7 +7,6 @@ import { IEvent } from 'common/EventEmitter';
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types';
import { IBuffer } from 'common/buffer/Types';
import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';
-import { IMouseService, IRenderService } from './services/Services';
/**
* A portion of the public API that are implemented identially internally and simply passed through.
@@ -18,9 +17,9 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
screenElement: HTMLElement | undefined;
browser: IBrowser;
buffer: IBuffer;
+ linkifier: ILinkifier2 | undefined;
viewport: IViewport | undefined;
options: Required;
- linkifier2: ILinkifier2;
onBlur: IEvent;
onFocus: IEvent;
@@ -32,6 +31,7 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
}
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
+export type CustomWheelEventHandler = (event: WheelEvent) => boolean;
export type LineData = CharData[];
@@ -127,13 +127,6 @@ export interface ILinkifier2 extends IDisposable {
onShowLinkUnderline: IEvent;
onHideLinkUnderline: IEvent;
readonly currentLink: ILinkWithState | undefined;
-
- attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
- registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
-}
-
-interface ILinkProvider {
- provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
}
interface ILink {
diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts
index a8e1a498..cb0f35ea 100644
--- a/src/browser/Viewport.ts
+++ b/src/browser/Viewport.ts
@@ -36,6 +36,8 @@ export class Viewport extends Disposable implements IViewport {
private _activeBuffer: IBuffer;
private _renderDimensions: IRenderDimensions;
+ private _smoothScrollAnimationFrame: number = 0;
+
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
// quick fix and could have a more robust solution in place that reset the value when needed.
@@ -49,6 +51,8 @@ export class Viewport extends Disposable implements IViewport {
target: -1
};
+ private _ensureTimeout: number;
+
private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>());
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
@@ -81,7 +85,7 @@ export class Viewport extends Disposable implements IViewport {
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
// Perform this async to ensure the ICharSizeService is ready.
- setTimeout(() => this.syncScrollArea());
+ this._ensureTimeout = window.setTimeout(() => this.syncScrollArea());
}
private _handleThemeChange(colors: ReadonlyColorSet): void {
@@ -211,7 +215,12 @@ export class Viewport extends Disposable implements IViewport {
// Continue or finish smooth scroll
if (percent < 1) {
- this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll());
+ if (!this._smoothScrollAnimationFrame) {
+ this._smoothScrollAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
+ this._smoothScrollAnimationFrame = 0;
+ this._smoothScroll();
+ });
+ }
} else {
this._clearSmoothScrollState();
}
@@ -398,4 +407,8 @@ export class Viewport extends Disposable implements IViewport {
this._viewportElement.scrollTop += deltaY;
return this._bubbleScroll(ev, deltaY);
}
+
+ public dispose(): void {
+ clearTimeout(this._ensureTimeout);
+ }
}
diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts
index 7542969a..9891709f 100644
--- a/src/browser/input/CompositionHelper.ts
+++ b/src/browser/input/CompositionHelper.ts
@@ -159,8 +159,9 @@ export class CompositionHelper {
// otherwise input characters can be duplicated. (Issue #3191)
currentCompositionPosition.start += this._dataAlreadySent.length;
if (this._isComposing) {
- // Use the end position to get the string if a new composition has started.
- input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
+ // Use the start position of the new composition to get the string
+ // if a new composition has started.
+ input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
} else {
// Don't use the end position here in order to pick up any characters after the
// composition has finished, for example when typing a non-composition character
diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts
index 6f009f74..56edbc50 100644
--- a/src/browser/public/Terminal.ts
+++ b/src/browser/public/Terminal.ts
@@ -20,6 +20,8 @@ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOption
*/
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
+let $value = 0;
+
export class Terminal extends Disposable implements ITerminalApi {
private _core: ITerminal;
private _addonManager: AddonManager;
@@ -138,6 +140,9 @@ export class Terminal extends Disposable implements ITerminalApi {
public focus(): void {
this._core.focus();
}
+ public input(data: string, wasUserInput: boolean = true): void {
+ this._core.input(data, wasUserInput);
+ }
public resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
@@ -148,6 +153,9 @@ export class Terminal extends Disposable implements ITerminalApi {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
+ public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void {
+ this._core.attachCustomWheelEventHandler(customWheelEventHandler);
+ }
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
return this._core.registerLinkProvider(linkProvider);
}
@@ -243,16 +251,16 @@ export class Terminal extends Disposable implements ITerminalApi {
}
private _verifyIntegers(...values: number[]): void {
- for (const value of values) {
- if (value === Infinity || isNaN(value) || value % 1 !== 0) {
+ for ($value of values) {
+ if ($value === Infinity || isNaN($value) || $value % 1 !== 0) {
throw new Error('This API only accepts integers');
}
}
}
private _verifyPositiveIntegers(...values: number[]): void {
- for (const value of values) {
- if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) {
+ for ($value of values) {
+ if ($value && ($value === Infinity || isNaN($value) || $value % 1 !== 0 || $value < 0)) {
throw new Error('This API only accepts positive integers');
}
}
diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts
index 1549b130..92d152f0 100644
--- a/src/browser/renderer/dom/DomRenderer.ts
+++ b/src/browser/renderer/dom/DomRenderer.ts
@@ -183,14 +183,23 @@ export class DomRenderer extends Disposable implements IRenderer {
` font-style: italic;` +
`}`;
// Blink animation
+ const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`;
+ const blinkAnimationBarId = `blink_bar_${this._terminalClass}`;
+ const blinkAnimationBlockId = `blink_block_${this._terminalClass}`;
styles +=
- `@keyframes blink_box_shadow` + `_` + this._terminalClass + ` {` +
+ `@keyframes ${blinkAnimationUnderlineId} {` +
` 50% {` +
` border-bottom-style: hidden;` +
` }` +
`}`;
styles +=
- `@keyframes blink_block` + `_` + this._terminalClass + ` {` +
+ `@keyframes ${blinkAnimationBarId} {` +
+ ` 50% {` +
+ ` box-shadow: none;` +
+ ` }` +
+ `}`;
+ styles +=
+ `@keyframes ${blinkAnimationBlockId} {` +
` 0% {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
@@ -202,13 +211,23 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`;
// Cursor
styles +=
- `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}:not(.${RowCss.CURSOR_STYLE_BLOCK_CLASS}) {` +
- ` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
+ ` animation: ${blinkAnimationUnderlineId} 1s step-end infinite;` +
+ `}` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
+ ` animation: ${blinkAnimationBarId} 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
- ` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
+ ` animation: ${blinkAnimationBlockId} 1s step-end infinite;` +
`}` +
+ // !important helps fix an issue where the cursor will not render on top of the selection,
+ // however it's very hard to fix this issue and retain the blink animation without the use of
+ // !important. So this edge case fails when cursor blink is on.
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
+ ` background-color: ${colors.cursor.css};` +
+ ` color: ${colors.cursorAccent.css};` +
+ `}` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS}:not(.${RowCss.CURSOR_BLINK_CLASS}) {` +
` background-color: ${colors.cursor.css} !important;` +
` color: ${colors.cursorAccent.css} !important;` +
`}` +
@@ -324,6 +343,9 @@ export class DomRenderer extends Disposable implements IRenderer {
}
this._selectionRenderModel.update(this._terminal, start, end, columnSelectMode);
+ if (!this._selectionRenderModel.hasSelection) {
+ return;
+ }
// Translate from buffer position to viewport position
const viewportStartRow = this._selectionRenderModel.viewportStartRow;
@@ -331,11 +353,6 @@ export class DomRenderer extends Disposable implements IRenderer {
const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
- // No need to draw the selection
- if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
- return;
- }
-
// Create the selections
const documentFragment = this._document.createDocumentFragment();
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts
index 6ab68e7d..d71edeb9 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.ts
@@ -8,10 +8,10 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
-import { color, rgba } from 'common/Color';
+import { channels, color } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
-import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
+import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
import { WidthCache } from 'browser/renderer/dom/WidthCache';
import { IColorContrastCache } from 'browser/Types';
@@ -376,7 +376,7 @@ export class DomRendererRowFactory {
classes.push(`xterm-bg-${bg}`);
break;
case Attributes.CM_RGB:
- resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
+ resolvedBg = channels.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
break;
case Attributes.CM_DEFAULT:
@@ -408,7 +408,7 @@ export class DomRendererRowFactory {
}
break;
case Attributes.CM_RGB:
- const color = rgba.toColor(
+ const color = channels.toColor(
(fg >> 16) & 0xFF,
(fg >> 8) & 0xFF,
(fg ) & 0xFF
@@ -458,7 +458,7 @@ export class DomRendererRowFactory {
}
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
- if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) {
+ if (this._optionsService.rawOptions.minimumContrastRatio === 1 || treatGlyphAsBackgroundColor(cell.getCode())) {
return false;
}
diff --git a/src/browser/renderer/dom/WidthCache.ts b/src/browser/renderer/dom/WidthCache.ts
index 1527bad0..03d6cb70 100644
--- a/src/browser/renderer/dom/WidthCache.ts
+++ b/src/browser/renderer/dom/WidthCache.ts
@@ -134,9 +134,14 @@ export class WidthCache implements IDisposable {
public get(c: string, bold: boolean | number, italic: boolean | number): number {
let cp = 0;
if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) {
- return this._flat[cp] !== WidthCacheSettings.FLAT_UNSET
- ? this._flat[cp]
- : (this._flat[cp] = this._measure(c, 0));
+ if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) {
+ return this._flat[cp];
+ }
+ const width = this._measure(c, 0);
+ if (width > 0) {
+ this._flat[cp] = width;
+ }
+ return width;
}
let key = c;
if (bold) key += 'B';
@@ -147,7 +152,9 @@ export class WidthCache implements IDisposable {
if (bold) variant |= FontVariant.BOLD;
if (italic) variant |= FontVariant.ITALIC;
width = this._measure(c, variant);
- this._holey!.set(key, width);
+ if (width > 0) {
+ this._holey!.set(key, width);
+ }
}
return width;
}
diff --git a/src/browser/renderer/shared/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts
index 5837a675..6f61a704 100644
--- a/src/browser/renderer/shared/CellColorResolver.ts
+++ b/src/browser/renderer/shared/CellColorResolver.ts
@@ -5,6 +5,8 @@ import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle
import { IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData } from 'common/Types';
import { Terminal } from '@xterm/xterm';
+import { rgba } from 'common/Color';
+import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
// Work variables to avoid garbage collection
let $fg = 0;
@@ -65,11 +67,11 @@ export class CellColorResolver {
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
- $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
+ $bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasBg = true;
}
if (d.foregroundColorRGB) {
- $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
+ $fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
});
@@ -77,10 +79,94 @@ export class CellColorResolver {
// Apply the selection color if needed
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
if ($isSelected) {
- $bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
+ // If the cell has a bg color, retain the color by blending it with the selection color
+ if (
+ (this.result.fg & FgFlags.INVERSE) ||
+ (this.result.bg & Attributes.CM_MASK) !== Attributes.CM_DEFAULT
+ ) {
+ // Resolve the standard bg color
+ if (this.result.fg & FgFlags.INVERSE) {
+ switch (this.result.fg & Attributes.CM_MASK) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ $bg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
+ break;
+ case Attributes.CM_RGB:
+ $bg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF;
+ break;
+ case Attributes.CM_DEFAULT:
+ default:
+ $bg = this._themeService.colors.foreground.rgba;
+ }
+ } else {
+ switch (this.result.bg & Attributes.CM_MASK) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ $bg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
+ break;
+ case Attributes.CM_RGB:
+ $bg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF;
+ break;
+ // No need to consider default bg color here as it's not possible
+ }
+ }
+ // Blend with selection bg color
+ $bg = rgba.blend(
+ $bg,
+ ((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
+ ) >> 8 & Attributes.RGB_MASK;
+ } else {
+ $bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
+ }
$hasBg = true;
+
+ // Apply explicit selection foreground if present
if ($colors.selectionForeground) {
- $fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
+ $fg = $colors.selectionForeground.rgba >> 8 & Attributes.RGB_MASK;
+ $hasFg = true;
+ }
+
+ // Overwrite fg as bg if it's a special decorative glyph (eg. powerline)
+ if (treatGlyphAsBackgroundColor(cell.getCode())) {
+ // Inverse default background should be treated as transparent
+ if (
+ (this.result.fg & FgFlags.INVERSE) &&
+ (this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT
+ ) {
+ $fg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
+ } else {
+
+ if (this.result.fg & FgFlags.INVERSE) {
+ switch (this.result.bg & Attributes.CM_MASK) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ $fg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
+ break;
+ case Attributes.CM_RGB:
+ $fg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF;
+ break;
+ // No need to consider default bg color here as it's not possible
+ }
+ } else {
+ switch (this.result.fg & Attributes.CM_MASK) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ $fg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
+ break;
+ case Attributes.CM_RGB:
+ $fg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF;
+ break;
+ case Attributes.CM_DEFAULT:
+ default:
+ $fg = this._themeService.colors.foreground.rgba;
+ }
+ }
+
+ $fg = rgba.blend(
+ $fg,
+ ((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
+ ) >> 8 & Attributes.RGB_MASK;
+ }
$hasFg = true;
}
}
@@ -88,11 +174,11 @@ export class CellColorResolver {
// Apply decorations on the top layer
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
if (d.backgroundColorRGB) {
- $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
+ $bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasBg = true;
}
if (d.foregroundColorRGB) {
- $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
+ $fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
});
@@ -119,7 +205,7 @@ export class CellColorResolver {
if ($hasBg && !$hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
- $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
+ $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
@@ -128,7 +214,7 @@ export class CellColorResolver {
if (!$hasBg && $hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
- $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
+ $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
diff --git a/src/browser/renderer/shared/CustomGlyphs.ts b/src/browser/renderer/shared/CustomGlyphs.ts
index cf6292af..da9c3d36 100644
--- a/src/browser/renderer/shared/CustomGlyphs.ts
+++ b/src/browser/renderer/shared/CustomGlyphs.ts
@@ -355,6 +355,12 @@ const enum VectorType {
* Original symbols defined in https://github.com/powerline/fontpatcher
*/
export const powerlineDefinitions: { [index: string]: IVectorShape } = {
+ // Git branch
+ '\u{E0A0}': { d: 'M.3,1 L.03,1 L.03,.88 C.03,.82,.06,.78,.11,.73 C.15,.7,.2,.68,.28,.65 L.43,.6 C.49,.58,.53,.56,.56,.53 C.59,.5,.6,.47,.6,.43 L.6,.27 L.4,.27 L.69,.1 L.98,.27 L.78,.27 L.78,.46 C.78,.52,.76,.56,.72,.61 C.68,.66,.63,.67,.56,.7 L.48,.72 C.42,.74,.38,.76,.35,.78 C.32,.8,.31,.84,.31,.88 L.31,1 M.3,.5 L.03,.59 L.03,.09 L.3,.09 L.3,.655', type: VectorType.FILL },
+ // L N
+ '\u{E0A1}': { d: 'M.7,.4 L.7,.47 L.2,.47 L.2,.03 L.355,.03 L.355,.4 L.705,.4 M.7,.5 L.86,.5 L.86,.95 L.69,.95 L.44,.66 L.46,.86 L.46,.95 L.3,.95 L.3,.49 L.46,.49 L.71,.78 L.69,.565 L.69,.5', type: VectorType.FILL },
+ // Lock
+ '\u{E0A2}': { d: 'M.25,.94 C.16,.94,.11,.92,.11,.87 L.11,.53 C.11,.48,.15,.455,.23,.45 L.23,.3 C.23,.25,.26,.22,.31,.19 C.36,.16,.43,.15,.51,.15 C.59,.15,.66,.16,.71,.19 C.77,.22,.79,.26,.79,.3 L.79,.45 C.87,.45,.91,.48,.91,.53 L.91,.87 C.91,.92,.86,.94,.77,.94 L.24,.94 M.53,.2 C.49,.2,.45,.21,.42,.23 C.39,.25,.38,.27,.38,.3 L.38,.45 L.68,.45 L.68,.3 C.68,.27,.67,.25,.64,.23 C.61,.21,.58,.2,.53,.2 M.58,.82 L.58,.66 C.63,.65,.65,.63,.65,.6 C.65,.58,.64,.57,.61,.56 C.58,.55,.56,.54,.52,.54 C.48,.54,.46,.55,.43,.56 C.4,.57,.39,.59,.39,.6 C.39,.63,.41,.64,.46,.66 L.46,.82 L.57,.82', type: VectorType.FILL },
// Right triangle solid
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL, rightPadding: 2 },
// Right triangle line
diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts
index 59b87b0e..01064364 100644
--- a/src/browser/renderer/shared/RendererUtils.ts
+++ b/src/browser/renderer/shared/RendererUtils.ts
@@ -23,11 +23,44 @@ export function isRestrictedPowerlineGlyph(codepoint: number): boolean {
return 0xE0B0 <= codepoint && codepoint <= 0xE0B7;
}
+function isNerdFontGlyph(codepoint: number): boolean {
+ return 0xE000 <= codepoint && codepoint <= 0xF8FF;
+}
+
function isBoxOrBlockGlyph(codepoint: number): boolean {
return 0x2500 <= codepoint && codepoint <= 0x259F;
}
-export function excludeFromContrastRatioDemands(codepoint: number): boolean {
+export function isEmoji(codepoint: number): boolean {
+ return (
+ codepoint >= 0x1F600 && codepoint <= 0x1F64F || // Emoticons
+ codepoint >= 0x1F300 && codepoint <= 0x1F5FF || // Misc Symbols and Pictographs
+ codepoint >= 0x1F680 && codepoint <= 0x1F6FF || // Transport and Map
+ codepoint >= 0x2600 && codepoint <= 0x26FF || // Misc symbols
+ codepoint >= 0x2700 && codepoint <= 0x27BF || // Dingbats
+ codepoint >= 0xFE00 && codepoint <= 0xFE0F || // Variation Selectors
+ codepoint >= 0x1F900 && codepoint <= 0x1F9FF || // Supplemental Symbols and Pictographs
+ codepoint >= 0x1F1E6 && codepoint <= 0x1F1FF
+ );
+}
+
+export function allowRescaling(codepoint: number | undefined, width: number, glyphSizeX: number, deviceCellWidth: number): boolean {
+ return (
+ // Is single cell width
+ width === 1 &&
+ // Glyph exceeds cell bounds, add 50% to avoid hurting readability by rescaling glyphs that
+ // barely overlap
+ glyphSizeX > Math.ceil(deviceCellWidth * 1.5) &&
+ // Never rescale ascii
+ codepoint !== undefined && codepoint > 0xFF &&
+ // Never rescale emoji
+ !isEmoji(codepoint) &&
+ // Never rescale powerline or nerd fonts
+ !isPowerlineGlyph(codepoint) && !isNerdFontGlyph(codepoint)
+ );
+}
+
+export function treatGlyphAsBackgroundColor(codepoint: number): boolean {
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
}
diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts
index f3f67b8b..af2cafb8 100644
--- a/src/browser/renderer/shared/TextureAtlas.ts
+++ b/src/browser/renderer/shared/TextureAtlas.ts
@@ -6,16 +6,15 @@
import { IColorContrastCache } from 'browser/Types';
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
-import { computeNextVariantOffset, excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
+import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
-import { NULL_COLOR, color, rgba } from 'common/Color';
+import { NULL_COLOR, channels, color, rgba } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
import { FourKeyMap } from 'common/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue';
import { IColor } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
-import { traceCall } from 'common/services/LogService';
import { IUnicodeService } from 'common/services/Services';
/**
@@ -292,8 +291,7 @@ export class TextureAtlas implements ITextureAtlas {
break;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(bgColor);
- // TODO: This object creation is slow
- result = rgba.toColor(arr[0], arr[1], arr[2]);
+ result = channels.toColor(arr[0], arr[1], arr[2]);
break;
case Attributes.CM_DEFAULT:
default:
@@ -325,7 +323,7 @@ export class TextureAtlas implements ITextureAtlas {
break;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
- result = rgba.toColor(arr[0], arr[1], arr[2]);
+ result = channels.toColor(arr[0], arr[1], arr[2]);
break;
case Attributes.CM_DEFAULT:
default:
@@ -407,7 +405,7 @@ export class TextureAtlas implements ITextureAtlas {
return undefined;
}
- const color = rgba.toColor(
+ const color = channels.toColor(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
@@ -424,7 +422,6 @@ export class TextureAtlas implements ITextureAtlas {
return this._config.colors.contrastCache;
}
- @traceCall
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
@@ -492,7 +489,7 @@ export class TextureAtlas implements ITextureAtlas {
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0));
- const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
+ const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, treatGlyphAsBackgroundColor(chars.charCodeAt(0)));
this._tmpCtx.fillStyle = foregroundColor.css;
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts
index 614b9b30..da14b67d 100644
--- a/src/browser/services/CharSizeService.ts
+++ b/src/browser/services/CharSizeService.ts
@@ -8,12 +8,6 @@ import { EventEmitter } from 'common/EventEmitter';
import { ICharSizeService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
-
-const enum MeasureSettings {
- REPEAT = 32
-}
-
-
export class CharSizeService extends Disposable implements ICharSizeService {
public serviceBrand: undefined;
@@ -32,7 +26,11 @@ export class CharSizeService extends Disposable implements ICharSizeService {
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
- this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
+ try {
+ this._measureStrategy = this.register(new TextMetricsMeasureStrategy(this._optionsService));
+ } catch {
+ this._measureStrategy = this.register(new DomMeasureStrategy(document, parentElement, this._optionsService));
+ }
this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure()));
}
@@ -47,12 +45,7 @@ export class CharSizeService extends Disposable implements ICharSizeService {
}
interface IMeasureStrategy {
- measure(): IReadonlyMeasureResult;
-}
-
-interface IReadonlyMeasureResult {
- readonly width: number;
- readonly height: number;
+ measure(): Readonly;
}
interface IMeasureResult {
@@ -60,10 +53,26 @@ interface IMeasureResult {
height: number;
}
-// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses
-// ctx.measureText
-class DomMeasureStrategy implements IMeasureStrategy {
- private _result: IMeasureResult = { width: 0, height: 0 };
+const enum DomMeasureStrategyConstants {
+ REPEAT = 32
+}
+
+abstract class BaseMeasureStategy extends Disposable implements IMeasureStrategy {
+ protected _result: IMeasureResult = { width: 0, height: 0 };
+
+ protected _validateAndSet(width: number | undefined, height: number | undefined): void {
+ // If values are 0 then the element is likely currently display:none, in which case we should
+ // retain the previous value.
+ if (width !== undefined && width > 0 && height !== undefined && height > 0) {
+ this._result.width = width;
+ this._result.height = height;
+ }
+ }
+
+ public abstract measure(): Readonly;
+}
+
+class DomMeasureStrategy extends BaseMeasureStategy {
private _measureElement: HTMLElement;
constructor(
@@ -71,32 +80,48 @@ class DomMeasureStrategy implements IMeasureStrategy {
private _parentElement: HTMLElement,
private _optionsService: IOptionsService
) {
+ super();
this._measureElement = this._document.createElement('span');
this._measureElement.classList.add('xterm-char-measure-element');
- this._measureElement.textContent = 'W'.repeat(MeasureSettings.REPEAT);
+ this._measureElement.textContent = 'W'.repeat(DomMeasureStrategyConstants.REPEAT);
this._measureElement.setAttribute('aria-hidden', 'true');
this._measureElement.style.whiteSpace = 'pre';
this._measureElement.style.fontKerning = 'none';
this._parentElement.appendChild(this._measureElement);
}
- public measure(): IReadonlyMeasureResult {
+ public measure(): Readonly {
this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily;
this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`;
// Note that this triggers a synchronous layout
- const geometry = {
- height: Number(this._measureElement.offsetHeight),
- width: Number(this._measureElement.offsetWidth)
- };
-
- // If values are 0 then the element is likely currently display:none, in which case we should
- // retain the previous value.
- if (geometry.width !== 0 && geometry.height !== 0) {
- this._result.width = geometry.width / MeasureSettings.REPEAT;
- this._result.height = Math.ceil(geometry.height);
- }
+ this._validateAndSet(Number(this._measureElement.offsetWidth) / DomMeasureStrategyConstants.REPEAT, Number(this._measureElement.offsetHeight));
return this._result;
}
}
+
+class TextMetricsMeasureStrategy extends BaseMeasureStategy {
+ private _canvas: OffscreenCanvas;
+ private _ctx: OffscreenCanvasRenderingContext2D;
+
+ constructor(
+ private _optionsService: IOptionsService
+ ) {
+ super();
+ // This will throw if any required API is not supported
+ this._canvas = new OffscreenCanvas(100, 100);
+ this._ctx = this._canvas.getContext('2d')!;
+ const a = this._ctx.measureText('W');
+ if (!('width' in a && 'fontBoundingBoxAscent' in a && 'fontBoundingBoxDescent' in a)) {
+ throw new Error('Required font metrics not supported');
+ }
+ }
+
+ public measure(): Readonly {
+ this._ctx.font = `${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`;
+ const metrics = this._ctx.measureText('W');
+ this._validateAndSet(metrics.width, metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent);
+ return this._result;
+ }
+}
diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts
index 575b62b6..a6c066b2 100644
--- a/src/browser/services/CoreBrowserService.ts
+++ b/src/browser/services/CoreBrowserService.ts
@@ -13,7 +13,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
private _isFocused = false;
private _cachedIsFocused: boolean | undefined = undefined;
- private _screenDprMonitor = new ScreenDprMonitor(this._window);
+ private _screenDprMonitor = this.register(new ScreenDprMonitor(this._window));
private readonly _onDprChange = this.register(new EventEmitter());
public readonly onDprChange = this._onDprChange.event;
@@ -31,8 +31,12 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
this.register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w)));
this.register(forwardEvent(this._screenDprMonitor.onDprChange, this._onDprChange));
- this._textarea.addEventListener('focus', () => this._isFocused = true);
- this._textarea.addEventListener('blur', () => this._isFocused = false);
+ this.register(
+ addDisposableDomListener(this._textarea, 'focus', () => (this._isFocused = true))
+ );
+ this.register(
+ addDisposableDomListener(this._textarea, 'blur', () => (this._isFocused = false))
+ );
}
public get window(): Window & typeof globalThis {
diff --git a/src/browser/services/LinkProviderService.ts b/src/browser/services/LinkProviderService.ts
new file mode 100644
index 00000000..2590f24b
--- /dev/null
+++ b/src/browser/services/LinkProviderService.ts
@@ -0,0 +1,28 @@
+import { ILinkProvider, ILinkProviderService } from 'browser/services/Services';
+import { Disposable, toDisposable } from 'common/Lifecycle';
+import { IDisposable } from 'common/Types';
+
+export class LinkProviderService extends Disposable implements ILinkProviderService {
+ declare public serviceBrand: undefined;
+
+ public readonly linkProviders: ILinkProvider[] = [];
+
+ constructor() {
+ super();
+ this.register(toDisposable(() => this.linkProviders.length = 0));
+ }
+
+ public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
+ this.linkProviders.push(linkProvider);
+ return {
+ dispose: () => {
+ // Remove the link provider from the list
+ const providerIndex = this.linkProviders.indexOf(linkProvider);
+
+ if (providerIndex !== -1) {
+ this.linkProviders.splice(providerIndex, 1);
+ }
+ }
+ };
+ }
+}
diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts
index 9fa8d234..d4f2be46 100644
--- a/src/browser/services/RenderService.ts
+++ b/src/browser/services/RenderService.ts
@@ -8,9 +8,9 @@ import { IRenderDebouncerWithCallback } from 'browser/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { EventEmitter } from 'common/EventEmitter';
-import { Disposable, MutableDisposable } from 'common/Lifecycle';
+import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { DebouncedIdleTask } from 'common/TaskQueue';
-import { IBufferService, IDecorationService, IInstantiationService, IOptionsService } from 'common/services/Services';
+import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
interface ISelectionState {
start: [number, number] | undefined;
@@ -24,6 +24,7 @@ export class RenderService extends Disposable implements IRenderService {
private _renderer: MutableDisposable = this.register(new MutableDisposable());
private _renderDebouncer: IRenderDebouncerWithCallback;
private _pausedResizeTask = new DebouncedIdleTask();
+ private _observerDisposable = this.register(new MutableDisposable());
private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
@@ -38,7 +39,7 @@ export class RenderService extends Disposable implements IRenderService {
};
private readonly _onDimensionsChange = this.register(new EventEmitter());
- public readonly onDimensionsChange = this._onDimensionsChange.event;
+ public readonly onDimensionsChange = this._onDimensionsChange.event;
private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>());
public readonly onRenderedViewportChange = this._onRenderedViewportChange.event;
private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
@@ -56,12 +57,11 @@ export class RenderService extends Disposable implements IRenderService {
@IDecorationService decorationService: IDecorationService,
@IBufferService bufferService: IBufferService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
- @IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
) {
super();
- this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end));
+ this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), coreBrowserService);
this.register(this._renderDebouncer);
this.register(coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
@@ -87,7 +87,8 @@ export class RenderService extends Disposable implements IRenderService {
'fontSize',
'fontWeight',
'fontWeightBold',
- 'minimumContrastRatio'
+ 'minimumContrastRatio',
+ 'rescaleOverlappingGlyphs'
], () => {
this.clear();
this.handleResize(bufferService.cols, bufferService.rows);
@@ -102,12 +103,17 @@ export class RenderService extends Disposable implements IRenderService {
this.register(themeService.onChangeColors(() => this._fullRefresh()));
+ this._registerIntersectionObserver(coreBrowserService.window, screenElement);
+ this.register(coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement)));
+ }
+
+ private _registerIntersectionObserver(w: Window & typeof globalThis, screenElement: HTMLElement): void {
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
- if ('IntersectionObserver' in coreBrowserService.window) {
- const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
+ if ('IntersectionObserver' in w) {
+ const observer = new w.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
observer.observe(screenElement);
- this.register({ dispose: () => observer.disconnect() });
+ this._observerDisposable.value = toDisposable(() => observer.disconnect());
}
}
@@ -242,7 +248,7 @@ export class RenderService extends Disposable implements IRenderService {
return;
}
if (this._isPaused) {
- this._pausedResizeTask.set(() => this._renderer.value!.handleResize(cols, rows));
+ this._pausedResizeTask.set(() => this._renderer.value?.handleResize(cols, rows));
} else {
this._renderer.value.handleResize(cols, rows);
}
diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts
index 5c14fa8a..a82eabd0 100644
--- a/src/browser/services/Services.ts
+++ b/src/browser/services/Services.ts
@@ -5,7 +5,7 @@
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
-import { IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { AllColorIndex, IDisposable } from 'common/Types';
@@ -145,3 +145,14 @@ export interface IThemeService {
*/
modifyColors(callback: (colors: IColorSet) => void): void;
}
+
+
+export const ILinkProviderService = createDecorator('LinkProviderService');
+export interface ILinkProviderService extends IDisposable {
+ serviceBrand: undefined;
+ readonly linkProviders: ReadonlyArray;
+ registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
+}
+export interface ILinkProvider {
+ provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
+}
diff --git a/src/common/Color.test.ts b/src/common/Color.test.ts
index 082c81c3..b1683711 100644
--- a/src/common/Color.test.ts
+++ b/src/common/Color.test.ts
@@ -29,6 +29,25 @@ describe('Color', () => {
assert.equal(channels.toCss(0xf0, 0xf0, 0xf0), '#f0f0f0');
assert.equal(channels.toCss(0xff, 0xff, 0xff), '#ffffff');
});
+ it('should convert an rgba array to css hex string', () => {
+ assert.equal(channels.toCss(0x00, 0x00, 0x00, 0x00), '#00000000');
+ assert.equal(channels.toCss(0x10, 0x10, 0x10, 0x10), '#10101010');
+ assert.equal(channels.toCss(0x20, 0x20, 0x20, 0x20), '#20202020');
+ assert.equal(channels.toCss(0x30, 0x30, 0x30, 0x30), '#30303030');
+ assert.equal(channels.toCss(0x40, 0x40, 0x40, 0x40), '#40404040');
+ assert.equal(channels.toCss(0x50, 0x50, 0x50, 0x50), '#50505050');
+ assert.equal(channels.toCss(0x60, 0x60, 0x60, 0x60), '#60606060');
+ assert.equal(channels.toCss(0x70, 0x70, 0x70, 0x70), '#70707070');
+ assert.equal(channels.toCss(0x80, 0x80, 0x80, 0x80), '#80808080');
+ assert.equal(channels.toCss(0x90, 0x90, 0x90, 0x90), '#90909090');
+ assert.equal(channels.toCss(0xa0, 0xa0, 0xa0, 0xa0), '#a0a0a0a0');
+ assert.equal(channels.toCss(0xb0, 0xb0, 0xb0, 0xb0), '#b0b0b0b0');
+ assert.equal(channels.toCss(0xc0, 0xc0, 0xc0, 0xc0), '#c0c0c0c0');
+ assert.equal(channels.toCss(0xd0, 0xd0, 0xd0, 0xd0), '#d0d0d0d0');
+ assert.equal(channels.toCss(0xe0, 0xe0, 0xe0, 0xe0), '#e0e0e0e0');
+ assert.equal(channels.toCss(0xf0, 0xf0, 0xf0, 0xf0), '#f0f0f0f0');
+ assert.equal(channels.toCss(0xff, 0xff, 0xff, 0xff), '#ffffffff');
+ });
});
describe('toRgba', () => {
@@ -71,6 +90,47 @@ describe('Color', () => {
assert.equal(channels.toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff);
});
});
+
+ describe('toColor', () => {
+ it('should convert an rgb array to an IColor', () => {
+ assert.deepStrictEqual(channels.toColor(0x00, 0x00, 0x00), { css: '#000000', rgba: 0x000000FF });
+ assert.deepStrictEqual(channels.toColor(0x10, 0x10, 0x10), { css: '#101010', rgba: 0x101010FF });
+ assert.deepStrictEqual(channels.toColor(0x20, 0x20, 0x20), { css: '#202020', rgba: 0x202020FF });
+ assert.deepStrictEqual(channels.toColor(0x30, 0x30, 0x30), { css: '#303030', rgba: 0x303030FF });
+ assert.deepStrictEqual(channels.toColor(0x40, 0x40, 0x40), { css: '#404040', rgba: 0x404040FF });
+ assert.deepStrictEqual(channels.toColor(0x50, 0x50, 0x50), { css: '#505050', rgba: 0x505050FF });
+ assert.deepStrictEqual(channels.toColor(0x60, 0x60, 0x60), { css: '#606060', rgba: 0x606060FF });
+ assert.deepStrictEqual(channels.toColor(0x70, 0x70, 0x70), { css: '#707070', rgba: 0x707070FF });
+ assert.deepStrictEqual(channels.toColor(0x80, 0x80, 0x80), { css: '#808080', rgba: 0x808080FF });
+ assert.deepStrictEqual(channels.toColor(0x90, 0x90, 0x90), { css: '#909090', rgba: 0x909090FF });
+ assert.deepStrictEqual(channels.toColor(0xa0, 0xa0, 0xa0), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
+ assert.deepStrictEqual(channels.toColor(0xb0, 0xb0, 0xb0), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
+ assert.deepStrictEqual(channels.toColor(0xc0, 0xc0, 0xc0), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
+ assert.deepStrictEqual(channels.toColor(0xd0, 0xd0, 0xd0), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
+ assert.deepStrictEqual(channels.toColor(0xe0, 0xe0, 0xe0), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
+ assert.deepStrictEqual(channels.toColor(0xf0, 0xf0, 0xf0), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
+ assert.deepStrictEqual(channels.toColor(0xff, 0xff, 0xff), { css: '#ffffff', rgba: 0xffffffFF });
+ });
+ it('should convert an rgba array to an IColor', () => {
+ assert.deepStrictEqual(channels.toColor(0x00, 0x00, 0x00, 0x00), { css: '#00000000', rgba: 0x00000000 });
+ assert.deepStrictEqual(channels.toColor(0x10, 0x10, 0x10, 0x10), { css: '#10101010', rgba: 0x10101010 });
+ assert.deepStrictEqual(channels.toColor(0x20, 0x20, 0x20, 0x20), { css: '#20202020', rgba: 0x20202020 });
+ assert.deepStrictEqual(channels.toColor(0x30, 0x30, 0x30, 0x30), { css: '#30303030', rgba: 0x30303030 });
+ assert.deepStrictEqual(channels.toColor(0x40, 0x40, 0x40, 0x40), { css: '#40404040', rgba: 0x40404040 });
+ assert.deepStrictEqual(channels.toColor(0x50, 0x50, 0x50, 0x50), { css: '#50505050', rgba: 0x50505050 });
+ assert.deepStrictEqual(channels.toColor(0x60, 0x60, 0x60, 0x60), { css: '#60606060', rgba: 0x60606060 });
+ assert.deepStrictEqual(channels.toColor(0x70, 0x70, 0x70, 0x70), { css: '#70707070', rgba: 0x70707070 });
+ assert.deepStrictEqual(channels.toColor(0x80, 0x80, 0x80, 0x80), { css: '#80808080', rgba: 0x80808080 });
+ assert.deepStrictEqual(channels.toColor(0x90, 0x90, 0x90, 0x90), { css: '#90909090', rgba: 0x90909090 });
+ assert.deepStrictEqual(channels.toColor(0xa0, 0xa0, 0xa0, 0xa0), { css: '#a0a0a0a0', rgba: 0xa0a0a0a0 });
+ assert.deepStrictEqual(channels.toColor(0xb0, 0xb0, 0xb0, 0xb0), { css: '#b0b0b0b0', rgba: 0xb0b0b0b0 });
+ assert.deepStrictEqual(channels.toColor(0xc0, 0xc0, 0xc0, 0xc0), { css: '#c0c0c0c0', rgba: 0xc0c0c0c0 });
+ assert.deepStrictEqual(channels.toColor(0xd0, 0xd0, 0xd0, 0xd0), { css: '#d0d0d0d0', rgba: 0xd0d0d0d0 });
+ assert.deepStrictEqual(channels.toColor(0xe0, 0xe0, 0xe0, 0xe0), { css: '#e0e0e0e0', rgba: 0xe0e0e0e0 });
+ assert.deepStrictEqual(channels.toColor(0xf0, 0xf0, 0xf0, 0xf0), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 });
+ assert.deepStrictEqual(channels.toColor(0xff, 0xff, 0xff, 0xff), { css: '#ffffffff', rgba: 0xffffffff });
+ });
+ });
});
describe('color', () => {
@@ -271,6 +331,27 @@ describe('Color', () => {
});
describe('rgba', () => {
+ describe('blend', () => {
+ it('should blend colors based on the alpha channel', () => {
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF00), 0x000000FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF10), 0x101010FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF20), 0x202020FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF30), 0x303030FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF40), 0x404040FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF50), 0x505050FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF60), 0x606060FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF70), 0x707070FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF80), 0x808080FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF90), 0x909090FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFA0), 0xA0A0A0FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFB0), 0xB0B0B0FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFC0), 0xC0C0C0FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFD0), 0xD0D0D0FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFE0), 0xE0E0E0FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFF0), 0xF0F0F0FF);
+ assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFFF), 0xFFFFFFFF);
+ });
+ });
describe('ensureContrastRatio', () => {
it('should return undefined if the color already meets the contrast ratio (black bg)', () => {
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 1), undefined);
diff --git a/src/common/Color.ts b/src/common/Color.ts
index 9bfed4e6..b7b3ff47 100644
--- a/src/common/Color.ts
+++ b/src/common/Color.ts
@@ -3,7 +3,6 @@
* @license MIT
*/
-import { isNode } from 'common/Platform';
import { IColor, IColorRGB } from 'common/Types';
let $r = 0;
@@ -33,6 +32,13 @@ export namespace channels {
// >>> 0 forces an unsigned int
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
+
+ export function toColor(r: number, g: number, b: number, a?: number): IColor {
+ return {
+ css: channels.toCss(r, g, b, a),
+ rgba: channels.toRgba(r, g, b, a)
+ };
+ }
}
/**
@@ -70,7 +76,7 @@ export namespace color {
if (!result) {
return undefined;
}
- return rgba.toColor(
+ return channels.toColor(
(result >> 24 & 0xFF),
(result >> 16 & 0xFF),
(result >> 8 & 0xFF)
@@ -110,9 +116,10 @@ export namespace color {
* '#rrggbbaa').
*/
export namespace css {
+ // Attempt to set get the shared canvas context
let $ctx: CanvasRenderingContext2D | undefined;
let $litmusColor: CanvasGradient | undefined;
- if (!isNode) {
+ try {
// This is guaranteed to run in the first window, so document should be correct
const canvas = document.createElement('canvas');
canvas.width = 1;
@@ -126,6 +133,9 @@ export namespace css {
$litmusColor = $ctx.createLinearGradient(0, 0, 1, 1);
}
}
+ catch {
+ // noop
+ }
/**
* Converts a css string to an IColor, this should handle all valid CSS color strings and will
@@ -142,14 +152,14 @@ export namespace css {
$r = parseInt(css.slice(1, 2).repeat(2), 16);
$g = parseInt(css.slice(2, 3).repeat(2), 16);
$b = parseInt(css.slice(3, 4).repeat(2), 16);
- return rgba.toColor($r, $g, $b);
+ return channels.toColor($r, $g, $b);
}
case 5: { // #rgba
$r = parseInt(css.slice(1, 2).repeat(2), 16);
$g = parseInt(css.slice(2, 3).repeat(2), 16);
$b = parseInt(css.slice(3, 4).repeat(2), 16);
$a = parseInt(css.slice(4, 5).repeat(2), 16);
- return rgba.toColor($r, $g, $b, $a);
+ return channels.toColor($r, $g, $b, $a);
}
case 7: // #rrggbb
return {
@@ -171,7 +181,7 @@ export namespace css {
$g = parseInt(rgbaMatch[2]);
$b = parseInt(rgbaMatch[3]);
$a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
- return rgba.toColor($r, $g, $b, $a);
+ return channels.toColor($r, $g, $b, $a);
}
// Validate the context is available for canvas-based color parsing
@@ -245,6 +255,23 @@ export namespace rgb {
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
*/
export namespace rgba {
+ export function blend(bg: number, fg: number): number {
+ $a = (fg & 0xFF) / 0xFF;
+ if ($a === 1) {
+ return fg;
+ }
+ const fgR = (fg >> 24) & 0xFF;
+ const fgG = (fg >> 16) & 0xFF;
+ const fgB = (fg >> 8) & 0xFF;
+ const bgR = (bg >> 24) & 0xFF;
+ const bgG = (bg >> 16) & 0xFF;
+ const bgB = (bg >> 8) & 0xFF;
+ $r = bgR + Math.round((fgR - bgR) * $a);
+ $g = bgG + Math.round((fgG - bgG) * $a);
+ $b = bgB + Math.round((fgB - bgB) * $a);
+ return channels.toRgba($r, $g, $b);
+ }
+
/**
* Given a foreground color and a background color, either increase or reduce the luminance of the
* foreground color until the specified contrast ratio is met. If pure white or black is hit
@@ -325,17 +352,9 @@ export namespace rgba {
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
- // FIXME: Move this to channels NS?
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, a?: number): IColor {
- return {
- css: channels.toCss(r, g, b, a),
- rgba: channels.toRgba(r, g, b, a)
- };
- }
}
export function toPaddedHex(c: number): string {
diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts
index 47f77406..327b8bc2 100644
--- a/src/common/CoreTerminal.ts
+++ b/src/common/CoreTerminal.ts
@@ -120,6 +120,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._oscLinkService = this._instantiationService.createInstance(OscLinkService);
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
+
// Register input handler and handle/forward events
this._inputHandler = this.register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService));
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
@@ -167,6 +168,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._writeBuffer.writeSync(data, maxSubsequentCalls);
}
+ public input(data: string, wasUserInput: boolean = true): void {
+ this.coreService.triggerDataEvent(data, wasUserInput);
+ }
+
public resize(x: number, y: number): void {
if (isNaN(x) || isNaN(y)) {
return;
diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts
index 589748a3..06ddf7fb 100644
--- a/src/common/EventEmitter.ts
+++ b/src/common/EventEmitter.ts
@@ -20,23 +20,18 @@ export interface IEventEmitter {
}
export class EventEmitter implements IEventEmitter {
- private _listeners: IListener[] = [];
+ private _listeners: Set> = new Set();
private _event?: IEvent;
private _disposed: boolean = false;
public get event(): IEvent {
if (!this._event) {
this._event = (listener: (arg1: T, arg2: U) => any) => {
- this._listeners.push(listener);
+ this._listeners.add(listener);
const disposable = {
dispose: () => {
if (!this._disposed) {
- for (let i = 0; i < this._listeners.length; i++) {
- if (this._listeners[i] === listener) {
- this._listeners.splice(i, 1);
- return;
- }
- }
+ this._listeners.delete(listener);
}
}
};
@@ -48,8 +43,8 @@ export class EventEmitter implements IEventEmitter {
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 (const l of this._listeners.values()) {
+ queue.push(l);
}
for (let i = 0; i < queue.length; i++) {
queue[i].call(undefined, arg1, arg2);
@@ -63,7 +58,7 @@ export class EventEmitter implements IEventEmitter {
public clearListeners(): void {
if (this._listeners) {
- this._listeners.length = 0;
+ this._listeners.clear();
}
}
}
diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts
index 8f9a988f..0f17a3e6 100644
--- a/src/common/InputHandler.test.ts
+++ b/src/common/InputHandler.test.ts
@@ -12,11 +12,12 @@ import { Attributes, BgFlags, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
-import { IBufferService, ICoreService } from 'common/services/Services';
+import { IBufferService, ICoreService, type IOscLinkService } from 'common/services/Services';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
import { BufferService } from 'common/services/BufferService';
import { CoreService } from 'common/services/CoreService';
+import { OscLinkService } from 'common/services/OscLinkService';
function getCursor(bufferService: IBufferService): number[] {
@@ -59,6 +60,7 @@ describe('InputHandler', () => {
let bufferService: IBufferService;
let coreService: ICoreService;
let optionsService: MockOptionsService;
+ let oscLinkService: IOscLinkService;
let inputHandler: TestInputHandler;
beforeEach(() => {
@@ -66,8 +68,9 @@ describe('InputHandler', () => {
bufferService = new BufferService(optionsService);
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
+ oscLinkService = new OscLinkService(bufferService);
- inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockCoreMouseService(), new MockUnicodeService());
});
describe('SL/SR/DECIC/DECDC', () => {
@@ -1982,6 +1985,32 @@ describe('InputHandler', () => {
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
stack.length = 0;
});
+ it('8: hyperlink with id', async () => {
+ await inputHandler.parseP('\x1b]8;id=100;http://localhost:3000\x07');
+ assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0);
+ assert.deepStrictEqual(
+ oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId),
+ {
+ id: '100',
+ uri: 'http://localhost:3000'
+ }
+ );
+ await inputHandler.parseP('\x1b]8;;\x07');
+ assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0);
+ });
+ it('8: hyperlink with semi-colon', async () => {
+ await inputHandler.parseP('\x1b]8;;http://localhost:3000;abc=def\x07');
+ assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0);
+ assert.deepStrictEqual(
+ oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId),
+ {
+ id: undefined,
+ uri: 'http://localhost:3000;abc=def'
+ }
+ );
+ await inputHandler.parseP('\x1b]8;;\x07');
+ assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0);
+ });
it('104: restore events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts
index a4b8c64b..6db8751e 100644
--- a/src/common/InputHandler.ts
+++ b/src/common/InputHandler.ts
@@ -2972,14 +2972,18 @@ export class InputHandler extends Disposable implements IInputHandler {
* feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink.
*/
public setHyperlink(data: string): boolean {
- const args = data.split(';');
- if (args.length < 2) {
- return false;
+ // Arg parsing is special cases to support unencoded semi-colons in the URIs (#4944)
+ const idx = data.indexOf(';');
+ if (idx === -1) {
+ // malformed sequence, just return as handled
+ return true;
}
- if (args[1]) {
- return this._createHyperlink(args[0], args[1]);
+ const id = data.slice(0, idx).trim();
+ const uri = data.slice(idx + 1);
+ if (uri) {
+ return this._createHyperlink(id, uri);
}
- if (args[0]) {
+ if (id.trim()) {
return false;
}
return this._finishHyperlink();
diff --git a/src/common/Platform.ts b/src/common/Platform.ts
index 1007fc0a..4102f20c 100644
--- a/src/common/Platform.ts
+++ b/src/common/Platform.ts
@@ -14,7 +14,7 @@ interface INavigator {
declare const navigator: INavigator;
declare const process: unknown;
-export const isNode = (typeof process !== 'undefined') ? true : false;
+export const isNode = (typeof process !== 'undefined' && 'title' in (process as any)) ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts
index c3250091..82b6dfa6 100644
--- a/src/common/SortedList.ts
+++ b/src/common/SortedList.ts
@@ -3,16 +3,27 @@
* @license MIT
*/
+import { IdleTaskQueue } from 'common/TaskQueue';
+
// Work variables to avoid garbage collection.
let i = 0;
/**
- * A generic list that is maintained in sorted order and allows values with duplicate keys. This
- * list is based on binary search and as such locating a key will take O(log n) amortized, this
- * includes the by key iterator.
+ * A generic list that is maintained in sorted order and allows values with duplicate keys. Deferred
+ * batch insertion and deletion is used to significantly reduce the time it takes to insert and
+ * delete a large amount of items in succession. This list is based on binary search and as such
+ * locating a key will take O(log n) amortized, this includes the by key iterator.
*/
export class SortedList {
- private readonly _array: T[] = [];
+ private _array: T[] = [];
+
+ private readonly _insertedValues: T[] = [];
+ private readonly _flushInsertedTask = new IdleTaskQueue();
+ private _isFlushingInserted = false;
+
+ private readonly _deletedIndices: number[] = [];
+ private readonly _flushDeletedTask = new IdleTaskQueue();
+ private _isFlushingDeleted = false;
constructor(
private readonly _getKey: (value: T) => number
@@ -21,18 +32,50 @@ export class SortedList {
public clear(): void {
this._array.length = 0;
+ this._insertedValues.length = 0;
+ this._flushInsertedTask.clear();
+ this._isFlushingInserted = false;
+ this._deletedIndices.length = 0;
+ this._flushDeletedTask.clear();
+ this._isFlushingDeleted = false;
}
public insert(value: T): void {
- if (this._array.length === 0) {
- this._array.push(value);
- return;
+ this._flushCleanupDeleted();
+ if (this._insertedValues.length === 0) {
+ this._flushInsertedTask.enqueue(() => this._flushInserted());
+ }
+ this._insertedValues.push(value);
+ }
+
+ private _flushInserted(): void {
+ const sortedAddedValues = this._insertedValues.sort((a, b) => this._getKey(a) - this._getKey(b));
+ let sortedAddedValuesIndex = 0;
+ let arrayIndex = 0;
+
+ const newArray = new Array(this._array.length + this._insertedValues.length);
+
+ for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) {
+ if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) <= this._getKey(this._array[arrayIndex])) {
+ newArray[newArrayIndex] = sortedAddedValues[sortedAddedValuesIndex];
+ sortedAddedValuesIndex++;
+ } else {
+ newArray[newArrayIndex] = this._array[arrayIndex++];
+ }
+ }
+
+ this._array = newArray;
+ this._insertedValues.length = 0;
+ }
+
+ private _flushCleanupInserted(): void {
+ if (!this._isFlushingInserted && this._insertedValues.length > 0) {
+ this._flushInsertedTask.flush();
}
- i = this._search(this._getKey(value));
- this._array.splice(i, 0, value);
}
public delete(value: T): boolean {
+ this._flushCleanupInserted();
if (this._array.length === 0) {
return false;
}
@@ -49,14 +92,43 @@ export class SortedList {
}
do {
if (this._array[i] === value) {
- this._array.splice(i, 1);
+ if (this._deletedIndices.length === 0) {
+ this._flushDeletedTask.enqueue(() => this._flushDeleted());
+ }
+ this._deletedIndices.push(i);
return true;
}
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
return false;
}
+ private _flushDeleted(): void {
+ this._isFlushingDeleted = true;
+ const sortedDeletedIndices = this._deletedIndices.sort((a, b) => a - b);
+ let sortedDeletedIndicesIndex = 0;
+ const newArray = new Array(this._array.length - sortedDeletedIndices.length);
+ let newArrayIndex = 0;
+ for (let i = 0; i < this._array.length; i++) {
+ if (sortedDeletedIndices[sortedDeletedIndicesIndex] === i) {
+ sortedDeletedIndicesIndex++;
+ } else {
+ newArray[newArrayIndex++] = this._array[i];
+ }
+ }
+ this._array = newArray;
+ this._deletedIndices.length = 0;
+ this._isFlushingDeleted = false;
+ }
+
+ private _flushCleanupDeleted(): void {
+ if (!this._isFlushingDeleted && this._deletedIndices.length > 0) {
+ this._flushDeletedTask.flush();
+ }
+ }
+
public *getKeyIterator(key: number): IterableIterator {
+ this._flushCleanupInserted();
+ this._flushCleanupDeleted();
if (this._array.length === 0) {
return;
}
@@ -73,6 +145,8 @@ export class SortedList {
}
public forEachByKey(key: number, callback: (value: T) => void): void {
+ this._flushCleanupInserted();
+ this._flushCleanupDeleted();
if (this._array.length === 0) {
return;
}
@@ -89,6 +163,8 @@ export class SortedList {
}
public values(): IterableIterator {
+ this._flushCleanupInserted();
+ this._flushCleanupDeleted();
// Duplicate the array to avoid issues when _array changes while iterating
return [...this._array].values();
}
diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts
index 175c47c3..17c7231a 100644
--- a/src/common/Types.d.ts
+++ b/src/common/Types.d.ts
@@ -110,8 +110,8 @@ export interface ICharset {
export type CharData = [number, string, number, number];
export interface IColor {
- css: string;
- rgba: number; // 32-bit int with rgba in each byte
+ readonly css: string;
+ readonly rgba: number; // 32-bit int with rgba in each byte
}
export type IColorRGB = [number, number, number];
diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts
index 250c96bc..1d2922e8 100644
--- a/src/common/buffer/Buffer.ts
+++ b/src/common/buffer/Buffer.ts
@@ -611,8 +611,8 @@ export class Buffer implements IBuffer {
this._isClearing = true;
for (let i = 0; i < this.markers.length; i++) {
this.markers[i].dispose();
- this.markers.splice(i--, 1);
}
+ this.markers.length = 0;
this._isClearing = false;
}
diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts
index da759152..c9be78af 100644
--- a/src/common/services/DecorationService.ts
+++ b/src/common/services/DecorationService.ts
@@ -45,7 +45,8 @@ export class DecorationService extends Disposable implements IDecorationService
const decoration = new Decoration(options);
if (decoration) {
const markerDispose = decoration.marker.onDispose(() => decoration.dispose());
- decoration.onDispose(() => {
+ const listener = decoration.onDispose(() => {
+ listener.dispose();
if (decoration) {
if (this._decorations.delete(decoration)) {
this._onDecorationRemoved.fire(decoration);
diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts
index eb9dbfa8..0375f6ad 100644
--- a/src/common/services/OptionsService.ts
+++ b/src/common/services/OptionsService.ts
@@ -4,7 +4,7 @@
*/
import { EventEmitter } from 'common/EventEmitter';
-import { Disposable } from 'common/Lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
import { isMac } from 'common/Platform';
import { CursorStyle, IDisposable } from 'common/Types';
import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services';
@@ -44,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly> = {
allowTransparency: false,
tabStopWidth: 8,
theme: {},
+ rescaleOverlappingGlyphs: false,
rightClickSelectsWord: isMac,
windowOptions: {},
windowsMode: false,
@@ -86,6 +87,13 @@ export class OptionsService extends Disposable implements IOptionsService {
this.rawOptions = defaultOptions;
this.options = { ... defaultOptions };
this._setupOptions();
+
+ // Clear out options that could link outside xterm.js as they could easily cause an embedder
+ // memory leak
+ this.register(toDisposable(() => {
+ this.rawOptions.linkHandler = null;
+ this.rawOptions.documentOverride = null;
+ }));
}
// eslint-disable-next-line @typescript-eslint/naming-convention
diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts
index 304e8cbb..210a0afb 100644
--- a/src/common/services/Services.ts
+++ b/src/common/services/Services.ts
@@ -234,6 +234,7 @@ export interface ITerminalOptions {
macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean;
minimumContrastRatio?: number;
+ rescaleOverlappingGlyphs?: boolean;
rightClickSelectsWord?: boolean;
rows?: number;
screenReaderMode?: boolean;
diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts
index 18000c8f..66040756 100644
--- a/src/headless/Terminal.ts
+++ b/src/headless/Terminal.ts
@@ -81,6 +81,10 @@ export class Terminal extends CoreTerminal {
this._onBell.fire();
}
+ public input(data: string, wasUserInput: boolean = true): void {
+ this.coreService.triggerDataEvent(data, wasUserInput);
+ }
+
/**
* Resizes the terminal.
*
diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts
index df202660..1b39c184 100644
--- a/src/headless/public/Terminal.ts
+++ b/src/headless/public/Terminal.ts
@@ -80,6 +80,7 @@ export class Terminal extends Disposable implements ITerminalApi {
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
public get onScroll(): IEvent { return this._core.onScroll; }
public get onTitleChange(): IEvent { return this._core.onTitleChange; }
+ public get onWriteParsed(): IEvent { return this._core.onWriteParsed; }
public get parser(): IParser {
this._checkProposedApi();
@@ -134,6 +135,9 @@ export class Terminal extends Disposable implements ITerminalApi {
this._publicOptions[propName] = options[propName];
}
}
+ public input(data: string, wasUserInput: boolean = true): void {
+ this._core.input(data, wasUserInput);
+ }
public resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
diff --git a/test/api/TestUtils.ts b/test/api/TestUtils.ts
index 9ebf67b6..cee59cce 100644
--- a/test/api/TestUtils.ts
+++ b/test/api/TestUtils.ts
@@ -74,9 +74,10 @@ export function getBrowserType(): playwright.BrowserType {
+export function launchBrowser(opts?: playwright.LaunchOptions): Promise {
const browserType = getBrowserType();
- const options: Record = {
+ const options: playwright.LaunchOptions = {
+ ...opts,
headless: process.argv.includes('--headless')
};
diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts
index 77bf7941..119832d6 100644
--- a/test/playwright/Renderer.test.ts
+++ b/test/playwright/Renderer.test.ts
@@ -8,7 +8,10 @@ import { ITestContext, createTestContext, openTerminal } from './TestUtils';
import { ISharedRendererTestContext, injectSharedRendererTestsStandalone, injectSharedRendererTests } from './SharedRendererTests';
let ctx: ITestContext;
-const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any;
+const ctxWrapper: ISharedRendererTestContext = {
+ value: undefined,
+ skipDomExceptions: true
+} as any;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
ctxWrapper.value = ctx;
@@ -18,5 +21,5 @@ test.afterAll(async () => await ctx.page.close());
test.describe('DOM Renderer Integration Tests', () => {
injectSharedRendererTests(ctxWrapper);
- injectSharedRendererTestsStandalone(ctxWrapper);
+ injectSharedRendererTestsStandalone(ctxWrapper, () => {});
});
diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts
index aa747277..68492b3d 100644
--- a/test/playwright/SharedRendererTests.ts
+++ b/test/playwright/SharedRendererTests.ts
@@ -11,6 +11,7 @@ import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate } f
export interface ISharedRendererTestContext {
value: ITestContext;
skipCanvasExceptions?: boolean;
+ skipDomExceptions?: boolean;
}
export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void {
@@ -945,7 +946,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 255, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 255, 0, 255]);
- await ctx.value.page.evaluate(`window.term.selectAll()`);
+ await ctx.value.proxy.selectAll();
frameDetails = undefined;
// Selection only cell needs to be first to ensure renderer has kicked in
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]);
@@ -965,7 +966,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
// Check both the cursor line and another line
await ctx.value.proxy.writeln('_ ');
await ctx.value.proxy.write('_ ');
- await ctx.value.page.evaluate(`window.term.selectAll()`);
+ await ctx.value.proxy.selectAll();
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [128, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [128, 0, 0, 255]);
@@ -980,6 +981,46 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
});
});
+ (ctx.skipCanvasExceptions || ctx.skipDomExceptions ? test.describe.skip : test.describe)('selection blending', () => {
+ test('background', async () => {
+ const theme: ITheme = {
+ red: '#CC0000',
+ selectionBackground: '#FFFFFF'
+ };
+ await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
+ await ctx.value.proxy.focus();
+ await ctx.value.proxy.writeln('\x1b[41m red bg\x1b[0m');
+ await ctx.value.proxy.writeln('\x1b[7m inverse\x1b[0m');
+ await ctx.value.proxy.writeln('\x1b[31;7m red fg inverse\x1b[0m');
+ await ctx.value.proxy.writeln('\x1b[48:2:0:204:0:0m red truecolor bg\x1b[0m');
+ await ctx.value.proxy.selectAll();
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [230, 128, 128, 255]);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [255, 255, 255, 255]);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [230, 128, 128, 255]);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 4), [230, 128, 128, 255]);
+ });
+ test('powerline decorative symbols', async () => {
+ const theme: ITheme = {
+ red: '#CC0000',
+ green: '#00CC00',
+ selectionBackground: '#FFFFFF'
+ };
+ await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
+ await ctx.value.proxy.focus();
+ await ctx.value.proxy.writeln('\u{E0B4} plain\x1b[0m');
+ await ctx.value.proxy.writeln('\x1b[31;42m\u{E0B4} red fg green bg\x1b[0m');
+ await ctx.value.proxy.writeln('\x1b[32;41m\u{E0B4} green fg red bg\x1b[0m');
+ await ctx.value.proxy.writeln('\x1b[31;42;7m\u{E0B4} red fg green bg inverse\x1b[0m');
+ await ctx.value.proxy.writeln('\x1b[32;41;7m\u{E0B4} green fg red bg inverse\x1b[0m');
+ await ctx.value.proxy.selectAll();
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255,255,255,255]);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [230, 128, 128, 255]);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [128, 230, 128, 255]);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 4), [128, 230, 128, 255]);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 5), [230, 128, 128, 255]);
+ });
+ });
+
test.describe('allowTransparency', async () => {
test.beforeEach(() => ctx.value.page.evaluate(`term.options.allowTransparency = true`));
@@ -1003,7 +1044,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
const data = `\x1b[7m■\x1b[0m`;
await ctx.value.proxy.write( data);
- await ctx.value.page.evaluate(`window.term.selectAll()`);
+ await ctx.value.proxy.selectAll();
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
});
});
@@ -1092,7 +1133,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
});
test.describe('regression tests', () => {
- test('#4736: inactive selection background should replace regular cell background color', async () => {
+ (ctx.skipCanvasExceptions ? test.skip : test)('#4736: inactive selection background should replace regular cell background color', async () => {
const theme: ITheme = {
selectionBackground: '#FF0000',
selectionInactiveBackground: '#0000FF'
@@ -1126,7 +1167,8 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 0, 255]);
});
- test('#4759: minimum contrast ratio should be respected on inverse text', async () => {
+ // HACK: It's not clear why DOM is failing here
+ (ctx.skipDomExceptions ? test.skip : test)('#4759: minimum contrast ratio should be respected on inverse text', async () => {
const theme: ITheme = {
foreground: '#aaaaaa',
background: '#333333'
@@ -1192,6 +1234,20 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, rows), [0, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, rows, CellColorPosition.FIRST), [0, 0, 255, 255]);
});
+ test('#4917 The selection should not be displayed if it is not within the scope of the viewport.', async () => {
+ const theme: ITheme = {
+ selectionBackground: '#FF0000'
+ };
+ await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
+ for (let index = 0; index < 160; index++) {
+ await ctx.value.proxy.writeln(``);
+ }
+ await ctx.value.proxy.scrollToBottom();
+ const rows = await ctx.value.proxy.buffer.active.length;
+ await ctx.value.proxy.selectLines(rows - 1, rows - 1);
+ await ctx.value.proxy.scrollLines(-2);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
+ });
});
}
@@ -1205,31 +1261,34 @@ enum CellColorPosition {
* This is much slower than just calling `Terminal.reset` but testing some features needs this
* treatment.
*/
-export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext): void {
- test.beforeEach(async () => {
- // Recreate terminal
- await openTerminal(ctx.value);
- ctx.value.page.evaluate(`
- window.term.options.minimumContrastRatio = 1;
- window.term.options.allowTransparency = false;
- window.term.options.theme = undefined;
- `);
- // Clear the cached screenshot before each test
- frameDetails = undefined;
- });
- test.describe('regression tests', () => {
- test('#4790: cursor should not be displayed before focusing', async () => {
- const theme: ITheme = {
- cursor: '#0000FF'
- };
- await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
- await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
- await ctx.value.proxy.focus();
+export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext, setupCb: () => Promise | void): void {
+ test.describe('standalone tests', () => {
+ test.beforeEach(async () => {
+ // Recreate terminal
+ await openTerminal(ctx.value);
+ await ctx.value.page.evaluate(`
+ window.term.options.minimumContrastRatio = 1;
+ window.term.options.allowTransparency = false;
+ window.term.options.theme = undefined;
+ `);
+ await setupCb();
+ // Clear the cached screenshot before each test
frameDetails = undefined;
- await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]);
- await ctx.value.proxy.blur();
- frameDetails = undefined;
- await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
+ });
+ test.describe('regression tests', () => {
+ test('#4790: cursor should not be displayed before focusing', async () => {
+ const theme: ITheme = {
+ cursor: '#0000FF'
+ };
+ await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
+ await ctx.value.proxy.focus();
+ frameDetails = undefined;
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]);
+ await ctx.value.proxy.blur();
+ frameDetails = undefined;
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
+ });
});
});
}
diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts
index 3e925f79..79408d41 100644
--- a/test/playwright/TestUtils.ts
+++ b/test/playwright/TestUtils.ts
@@ -75,6 +75,7 @@ type TerminalProxyCustomOverrides = 'buffer' | (
'options' |
'open' |
'attachCustomKeyEventHandler' |
+ 'attachCustomWheelEventHandler' |
'registerLinkProvider' |
'registerCharacterJoiner' |
'deregisterCharacterJoiner' |
@@ -215,6 +216,7 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi
return new Promise(r => term.writeln(typeof data === 'string' ? data : new Uint8Array(data), r));
}, [await this.getHandle(), typeof data === 'string' ? data : Array.from(data)] as const);
}
+ public async input(data: string, wasUserInput: boolean = true): Promise { return this.evaluate(([term]) => term.input(data, wasUserInput)); }
public async resize(cols: number, rows: number): Promise { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); }
public async registerMarker(y?: number | undefined): Promise { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); }
public async registerDecoration(decorationOptions: IDecorationOptions): Promise { return this._page.evaluate(([term, decorationOptions]) => term.registerDecoration(decorationOptions), [await this.getHandle(), decorationOptions] as const); }
@@ -490,9 +492,10 @@ export function getBrowserType(): playwright.BrowserType {
+export function launchBrowser(opts?: playwright.LaunchOptions): Promise {
const browserType = getBrowserType();
- const options: Record = {
+ const options: playwright.LaunchOptions = {
+ ...opts,
headless: process.argv.includes('--headless')
};
diff --git a/tsconfig.all.json b/tsconfig.all.json
index e2028479..d40761f3 100644
--- a/tsconfig.all.json
+++ b/tsconfig.all.json
@@ -9,6 +9,7 @@
{ "path": "./test/playwright" },
{ "path": "./addons/addon-attach" },
{ "path": "./addons/addon-canvas" },
+ { "path": "./addons/addon-clipboard" },
{ "path": "./addons/addon-fit" },
{ "path": "./addons/addon-image" },
{ "path": "./addons/addon-ligatures" },
diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts
index f8cef382..2d3329ed 100644
--- a/typings/xterm-headless.d.ts
+++ b/typings/xterm-headless.d.ts
@@ -140,6 +140,23 @@ declare module '@xterm/headless' {
*/
minimumContrastRatio?: number;
+ /**
+ * Whether to rescale glyphs horizontally that are a single cell wide but
+ * have glyphs that would overlap following cell(s). This typically happens
+ * for ambiguous width characters (eg. the roman numeral characters U+2160+)
+ * which aren't featured in monospace fonts. This is an important feature
+ * for achieving GB18030 compliance.
+ *
+ * The following glyphs will never be rescaled:
+ *
+ * - Emoji glyphs
+ * - Powerline glyphs
+ * - Nerd font glyphs
+ *
+ * Note that this doesn't work with the DOM renderer. The default is false.
+ */
+ rescaleOverlappingGlyphs?: boolean;
+
/**
* Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications.
@@ -156,7 +173,7 @@ declare module '@xterm/headless' {
/**
* The amount of scrollback in the terminal. Scrollback is the amount of
* rows that are retained when lines are scrolled beyond the initial
- * viewport.
+ * viewport. Defaults to 1000.
*/
scrollback?: number;
@@ -697,6 +714,17 @@ declare module '@xterm/headless' {
*/
onLineFeed: IEvent;
+ /**
+ * Adds an event listener for when data has been parsed by the terminal,
+ * after {@link write} is called. This event is useful to listen for any
+ * changes in the buffer.
+ *
+ * This fires at most once per frame, after data parsing completes. Note
+ * that this can fire when there are still writes pending if there is a lot
+ * of data.
+ */
+ onWriteParsed: IEvent;
+
/**
* Adds an event listener for when the terminal is resized. The event value
* contains the new size.
@@ -718,6 +746,18 @@ declare module '@xterm/headless' {
*/
onTitleChange: IEvent;
+ /**
+ * Input data to application side. The data is treated the same way input
+ * typed into the terminal would (ie. the {@link onData} event will fire).
+ * @param data The data to forward to the application.
+ * @param wasUserInput Whether the input is genuine user input. This is true
+ * by default and triggers additionalbehavior like focus or selection
+ * clearing. Set this to false if the data sent should not be treated like
+ * user input would, for example passing an escape sequence to the
+ * application.
+ */
+ input(data: string, wasUserInput?: boolean): void;
+
/**
* Resizes the terminal. It's best practice to debounce calls to resize,
* this will help ensure that the pty can respond to the resize event
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index 70b0c6d7..b6470275 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -47,11 +47,13 @@ declare module '@xterm/xterm' {
/**
* When enabled the cursor will be set to the beginning of the next line
- * with every new line. This is equivalent to sending '\r\n' for each '\n'.
- * Normally the termios settings of the underlying PTY deals with the
- * translation of '\n' to '\r\n' and this setting should not be used. If you
+ * with every new line. This is equivalent to sending `\r\n` for each `\n`.
+ * Normally the settings of the underlying PTY (`termios`) deal with the
+ * translation of `\n` to `\r\n` and this setting should not be used. If you
* deal with data from a non-PTY related source, this settings might be
* useful.
+ *
+ * @see https://pubs.opengroup.org/onlinepubs/007904975/basedefs/termios.h.html
*/
convertEol?: boolean;
@@ -209,6 +211,23 @@ declare module '@xterm/xterm' {
*/
minimumContrastRatio?: number;
+ /**
+ * Whether to rescale glyphs horizontally that are a single cell wide but
+ * have glyphs that would overlap following cell(s). This typically happens
+ * for ambiguous width characters (eg. the roman numeral characters U+2160+)
+ * which aren't featured in monospace fonts. This is an important feature
+ * for achieving GB18030 compliance.
+ *
+ * The following glyphs will never be rescaled:
+ *
+ * - Emoji glyphs
+ * - Powerline glyphs
+ * - Nerd font glyphs
+ *
+ * Note that this doesn't work with the DOM renderer. The default is false.
+ */
+ rescaleOverlappingGlyphs?: boolean;
+
/**
* Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications.
@@ -225,7 +244,7 @@ declare module '@xterm/xterm' {
/**
* The amount of scrollback in the terminal. Scrollback is the amount of
* rows that are retained when lines are scrolled beyond the initial
- * viewport.
+ * viewport. Defaults to 1000.
*/
scrollback?: number;
@@ -963,6 +982,18 @@ declare module '@xterm/xterm' {
*/
focus(): void;
+ /**
+ * Input data to application side. The data is treated the same way input
+ * typed into the terminal would (ie. the {@link onData} event will fire).
+ * @param data The data to forward to the application.
+ * @param wasUserInput Whether the input is genuine user input. This is true
+ * by default and triggers additionalbehavior like focus or selection
+ * clearing. Set this to false if the data sent should not be treated like
+ * user input would, for example passing an escape sequence to the
+ * application.
+ */
+ input(data: string, wasUserInput?: boolean): void;
+
/**
* Resizes the terminal. It's best practice to debounce calls to resize,
* this will help ensure that the pty can respond to the resize event
@@ -1010,6 +1041,28 @@ declare module '@xterm/xterm' {
*/
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
+ /**
+ * Attaches a custom wheel event handler which is run before keys are
+ * processed, giving consumers of xterm.js control over whether to proceed
+ * or cancel terminal wheel events.
+ * @param customWheelEventHandler The custom WheelEvent handler to attach.
+ * This is a function that takes a WheelEvent, allowing consumers to stop
+ * propagation and/or prevent the default action. The function returns
+ * whether the event should be processed by xterm.js.
+ *
+ * @example A handler that prevents all wheel events while ctrl is held from
+ * being processed.
+ * ```ts
+ * term.attachCustomWheelEventHandler(ev => {
+ * if (ev.ctrlKey) {
+ * return false;
+ * }
+ * return true;
+ * });
+ * ```
+ */
+ attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void;
+
/**
* Registers a link provider, allowing a custom parser to be used to match
* and handle links. Multiple link providers can be used, they will be asked
diff --git a/webpack.config.headless.js b/webpack.config.headless.js
index 9e9099cd..12e7484d 100644
--- a/webpack.config.headless.js
+++ b/webpack.config.headless.js
@@ -39,8 +39,10 @@ const config = {
path: path.resolve('./headless/lib-headless'),
library: {
type: 'commonjs'
- }
+ },
+ // Force usage of globalThis instead of global / self. (This is cross-env compatible)
+ globalObject: 'globalThis',
},
- mode: 'production'
+ mode: 'production',
};
module.exports = config;
diff --git a/yarn.lock b/yarn.lock
index a0cb39da..e566fff2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -265,14 +265,14 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
-"@es-joy/jsdoccomment@~0.36.1":
- version "0.36.1"
- resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz#c37db40da36e4b848da5fd427a74bae3b004a30f"
- integrity sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg==
+"@es-joy/jsdoccomment@~0.41.0":
+ version "0.41.0"
+ resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz#4a2f7db42209c0425c71a1476ef1bdb6dcd836f6"
+ integrity sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==
dependencies:
- comment-parser "1.3.1"
- esquery "^1.4.0"
- jsdoc-type-pratt-parser "~3.1.0"
+ comment-parser "1.4.1"
+ esquery "^1.5.0"
+ jsdoc-type-pratt-parser "~4.0.0"
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
@@ -281,15 +281,20 @@
dependencies:
eslint-visitor-keys "^3.3.0"
-"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.5.1":
+"@eslint-community/regexpp@^4.5.1":
version "4.6.2"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==
-"@eslint/eslintrc@^2.1.0":
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d"
- integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==
+"@eslint-community/regexpp@^4.6.1":
+ version "4.10.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
+ integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
+
+"@eslint/eslintrc@^2.1.4":
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
+ integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@@ -301,17 +306,17 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@8.44.0":
- version "8.44.0"
- resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af"
- integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==
+"@eslint/js@8.56.0":
+ version "8.56.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b"
+ integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==
-"@humanwhocodes/config-array@^0.11.10":
- version "0.11.10"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
- integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==
+"@humanwhocodes/config-array@^0.11.13":
+ version "0.11.13"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297"
+ integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==
dependencies:
- "@humanwhocodes/object-schema" "^1.2.1"
+ "@humanwhocodes/object-schema" "^2.0.1"
debug "^4.1.1"
minimatch "^3.0.5"
@@ -320,10 +325,10 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
-"@humanwhocodes/object-schema@^1.2.1":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
- integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
+"@humanwhocodes/object-schema@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044"
+ integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
@@ -756,6 +761,11 @@
"@typescript-eslint/types" "6.2.0"
eslint-visitor-keys "^3.4.1"
+"@ungap/structured-clone@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
+ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
@@ -970,7 +980,7 @@ ajv-keywords@^3.5.2:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
-ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5:
+ajv@^6.12.4, ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -1029,6 +1039,11 @@ archy@^1.0.0:
resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==
+are-docs-informative@^0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963"
+ integrity sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==
+
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -1084,13 +1099,13 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-body-parser@1.20.1:
- version "1.20.1"
- resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
- integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
+body-parser@1.20.2:
+ version "1.20.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
+ integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
dependencies:
bytes "3.1.2"
- content-type "~1.0.4"
+ content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
@@ -1098,7 +1113,7 @@ body-parser@1.20.1:
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
- raw-body "2.5.1"
+ raw-body "2.5.2"
type-is "~1.6.18"
unpipe "1.0.0"
@@ -1149,6 +1164,11 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+builtin-modules@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
+ integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
+
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@@ -1355,10 +1375,10 @@ commander@^7.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
-comment-parser@1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b"
- integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==
+comment-parser@1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.1.tgz#bdafead37961ac079be11eb7ec65c4d021eaf9cc"
+ integrity sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==
commondir@^1.0.1:
version "1.0.1"
@@ -1382,7 +1402,7 @@ content-disposition@0.5.4:
dependencies:
safe-buffer "5.2.1"
-content-type@~1.0.4:
+content-type@~1.0.4, content-type@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
@@ -1397,10 +1417,10 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
-cookie@0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
- integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
+cookie@0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
+ integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
cross-env@^7.0.3:
version "7.0.3"
@@ -1666,18 +1686,20 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
-eslint-plugin-jsdoc@^39.3.6:
- version "39.9.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.9.1.tgz#e9ce1723411fd7ea0933b3ef0dd02156ae3068e2"
- integrity sha512-Rq2QY6BZP2meNIs48aZ3GlIlJgBqFCmR55+UBvaDkA3ZNQ0SvQXOs2QKkubakEijV8UbIVbVZKsOVN8G3MuqZw==
+eslint-plugin-jsdoc@^46.9.1:
+ version "46.9.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.1.tgz#d30adce51fecc768e87481bf4de46b8618c3d50e"
+ integrity sha512-11Ox5LCl2wY7gGkp9UOyew70o9qvii1daAH+h/MFobRVRNcy7sVlH+jm0HQdgcvcru6285GvpjpUyoa051j03Q==
dependencies:
- "@es-joy/jsdoccomment" "~0.36.1"
- comment-parser "1.3.1"
+ "@es-joy/jsdoccomment" "~0.41.0"
+ are-docs-informative "^0.0.2"
+ comment-parser "1.4.1"
debug "^4.3.4"
escape-string-regexp "^4.0.0"
- esquery "^1.4.0"
- semver "^7.3.8"
- spdx-expression-parse "^3.0.1"
+ esquery "^1.5.0"
+ is-builtin-module "^3.2.1"
+ semver "^7.5.4"
+ spdx-expression-parse "^4.0.0"
eslint-scope@5.1.1:
version "5.1.1"
@@ -1687,10 +1709,10 @@ eslint-scope@5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
-eslint-scope@^7.2.0:
- version "7.2.1"
- resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.1.tgz#936821d3462675f25a18ac5fd88a67cc15b393bd"
- integrity sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==
+eslint-scope@^7.2.2:
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
+ integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
@@ -1700,27 +1722,33 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
-eslint@^8.45.0:
- version "8.45.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.45.0.tgz#bab660f90d18e1364352c0a6b7c6db8edb458b78"
- integrity sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==
+eslint-visitor-keys@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
+ integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
+
+eslint@^8.56.0:
+ version "8.56.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15"
+ integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
- "@eslint-community/regexpp" "^4.4.0"
- "@eslint/eslintrc" "^2.1.0"
- "@eslint/js" "8.44.0"
- "@humanwhocodes/config-array" "^0.11.10"
+ "@eslint-community/regexpp" "^4.6.1"
+ "@eslint/eslintrc" "^2.1.4"
+ "@eslint/js" "8.56.0"
+ "@humanwhocodes/config-array" "^0.11.13"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
- ajv "^6.10.0"
+ "@ungap/structured-clone" "^1.2.0"
+ ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
- eslint-scope "^7.2.0"
- eslint-visitor-keys "^3.4.1"
- espree "^9.6.0"
+ eslint-scope "^7.2.2"
+ eslint-visitor-keys "^3.4.3"
+ espree "^9.6.1"
esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
@@ -1743,7 +1771,7 @@ eslint@^8.45.0:
strip-ansi "^6.0.1"
text-table "^0.2.0"
-espree@^9.6.0:
+espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
@@ -1757,7 +1785,7 @@ esprima@^4.0.0, esprima@^4.0.1:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-esquery@^1.4.0, esquery@^1.4.2:
+esquery@^1.4.2, esquery@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
@@ -1804,16 +1832,16 @@ express-ws@^5.0.2:
ws "^7.4.6"
express@^4.17.1:
- version "4.18.2"
- resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
- integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
+ version "4.19.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
+ integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
- body-parser "1.20.1"
+ body-parser "1.20.2"
content-disposition "0.5.4"
content-type "~1.0.4"
- cookie "0.5.0"
+ cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
@@ -2341,6 +2369,13 @@ is-boolean-object@^1.1.0:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
+is-builtin-module@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169"
+ integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==
+ dependencies:
+ builtin-modules "^3.3.0"
+
is-callable@^1.1.3:
version "1.2.7"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
@@ -2599,10 +2634,10 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
-jsdoc-type-pratt-parser@~3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz#a4a56bdc6e82e5865ffd9febc5b1a227ff28e67e"
- integrity sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw==
+jsdoc-type-pratt-parser@~4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz#136f0571a99c184d84ec84662c45c29ceff71114"
+ integrity sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==
jsdom@^18.0.1:
version "18.1.1"
@@ -3207,10 +3242,10 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-raw-body@2.5.1:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
- integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
+raw-body@2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+ integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
@@ -3348,7 +3383,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-semver@^7.3.4, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4:
+semver@^7.3.4, semver@^7.5.3, semver@^7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
@@ -3490,10 +3525,10 @@ spdx-exceptions@^2.1.0:
resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
-spdx-expression-parse@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
- integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
+spdx-expression-parse@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz#a23af9f3132115465dac215c099303e4ceac5794"
+ integrity sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==
dependencies:
spdx-exceptions "^2.1.0"
spdx-license-ids "^3.0.0"