Compare commits

..
Author SHA1 Message Date
Cursor AgentandDaniel Imms 511ce3f5d3 Remove unused type imports from common/Types.ts
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 18:43:00 +00:00
Cursor AgentandDaniel Imms cf68f0b514 Restore tsconfig files to match master (undo whitespace-only changes)
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 18:39:14 +00:00
Cursor AgentandDaniel Imms bd8510e3ab Move primitive types from common/Types.ts to common/primitives/Types.ts
Extract low-level types (IParams, ParamsArray, IDisposable, color,
keyboard, mouse, charset, etc.) into primitives/Types.ts. Keep
runtime/integration types (ITerminalOptions, IInputHandler, terminal
modes) in common/Types.ts. Lifecycle re-exports IDisposable from
primitives/Types.ts.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 18:32:48 +00:00
Cursor AgentandDaniel Imms f6c1b21258 Revert esbuild.mjs entry point split for primitives folder
src/common/**/*.ts already covers primitives since they remain under
common/. No build configuration changes are needed for this move.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 18:29:16 +00:00
Cursor AgentandDaniel Imms b7d45bfefa Revert tsconfig path mapping changes for primitives folder
Primitives remain under src/common/, so common/* aliases do not need
a separate primitives entry. Restore original tsconfig paths.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 18:24:02 +00:00
Cursor AgentandDaniel Imms 701c0affd5 Move common primitives to src/common/primitives/
Phase 1 of #5963: reorganize building blocks (buffer, parser, input,
data, and shared utilities) under common/primitives/. Runtime code
(services, public API, CoreTerminal, InputHandler) stays in place.

Update import paths across browser, headless, addons, and tests.
Add TypeScript path mappings so common/* aliases resolve primitives
first, then the common root.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 18:21:03 +00:00
216 changed files with 1796 additions and 3023 deletions
+17 -32
View File
@@ -6,19 +6,14 @@ on:
pull_request: pull_request:
branches: [ "master" ] branches: [ "master" ]
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: actions/checkout@v5
with:
persist-credentials: false
- name: Use Node.js 22.x - name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 uses: actions/setup-node@v5
with: with:
node-version: 22.x node-version: 22.x
cache: 'npm' cache: 'npm'
@@ -74,7 +69,7 @@ jobs:
./addons/addon-webgl/out/* \ ./addons/addon-webgl/out/* \
./addons/addon-webgl/out-*st/* ./addons/addon-webgl/out-*st/*
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 uses: actions/upload-artifact@v6
with: with:
name: build-artifacts name: build-artifacts
path: compressed-build.zip path: compressed-build.zip
@@ -84,11 +79,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: actions/checkout@v5
with:
persist-credentials: false
- name: Use Node.js 22.x - name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 uses: actions/setup-node@v5
with: with:
node-version: 22.x node-version: 22.x
cache: 'npm' cache: 'npm'
@@ -107,18 +100,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: actions/checkout@v5
with:
persist-credentials: false
- name: Use Node.js 22.x - name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 uses: actions/setup-node@v5
with: with:
node-version: 22.x node-version: 22.x
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: | run: |
npm ci npm ci
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 - uses: actions/download-artifact@v7
with: with:
name: build-artifacts name: build-artifacts
- name: Unzip artifacts - name: Unzip artifacts
@@ -146,18 +137,16 @@ jobs:
runs-on: [ubuntu, macos, windows] runs-on: [ubuntu, macos, windows]
runs-on: ${{ matrix.runs-on }}-latest runs-on: ${{ matrix.runs-on }}-latest
steps: steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: actions/checkout@v5
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}.x - name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 uses: actions/setup-node@v5
with: with:
node-version: ${{ matrix.node-version }}.x node-version: ${{ matrix.node-version }}.x
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: | run: |
npm ci npm ci
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 - uses: actions/download-artifact@v7
with: with:
name: build-artifacts name: build-artifacts
- name: Unzip artifacts - name: Unzip artifacts
@@ -182,11 +171,9 @@ jobs:
browser: [chromium, firefox, webkit] browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: actions/checkout@v5
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}.x - name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 uses: actions/setup-node@v5
with: with:
node-version: ${{ matrix.node-version }}.x node-version: ${{ matrix.node-version }}.x
cache: 'npm' cache: 'npm'
@@ -195,7 +182,7 @@ jobs:
npm ci npm ci
- name: Install playwright - name: Install playwright
run: npx playwright install --with-deps ${{ matrix.browser }} run: npx playwright install --with-deps ${{ matrix.browser }}
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 - uses: actions/download-artifact@v7
with: with:
name: build-artifacts name: build-artifacts
- name: Unzip artifacts - name: Unzip artifacts
@@ -245,11 +232,9 @@ jobs:
matrix: matrix:
node-version: [22] node-version: [22]
steps: steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: actions/checkout@v5
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}.x - name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 uses: actions/setup-node@v5
with: with:
node-version: ${{ matrix.node-version }}.x node-version: ${{ matrix.node-version }}.x
cache: 'npm' cache: 'npm'
@@ -258,7 +243,7 @@ jobs:
npm ci npm ci
- name: Install playwright - name: Install playwright
run: npx playwright install run: npx playwright install
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 - uses: actions/download-artifact@v7
with: with:
name: build-artifacts name: build-artifacts
- name: Unzip artifacts - name: Unzip artifacts
+4 -11
View File
@@ -8,11 +8,6 @@ on:
schedule: schedule:
- cron: '41 17 * * 0' - cron: '41 17 * * 0'
permissions:
actions: read
contents: read
security-events: write
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@@ -29,14 +24,12 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 uses: actions/checkout@v5
with:
persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 uses: github/codeql-action/analyze@v4
+2 -7
View File
@@ -9,9 +9,6 @@ on:
paths: paths:
- .github/workflows/copilot-setup-steps.yml - .github/workflows/copilot-setup-steps.yml
permissions:
contents: read
jobs: jobs:
copilot-setup-steps: copilot-setup-steps:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -21,11 +18,9 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 uses: actions/checkout@v5
with:
persist-credentials: false
- name: Use Node.js 22.x - name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 uses: actions/setup-node@v5
with: with:
node-version: 22.x node-version: 22.x
cache: 'npm' cache: 'npm'
+2 -7
View File
@@ -6,18 +6,13 @@ on:
# without running tests to save time to publish # without running tests to save time to publish
branches: [ "master" ] branches: [ "master" ]
permissions:
contents: read
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: actions/checkout@v5
with:
persist-credentials: false
- name: Use Node.js 22.x - name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 uses: actions/setup-node@v5
with: with:
node-version: 22.x node-version: 22.x
cache: 'npm' cache: 'npm'
-7
View File
@@ -254,13 +254,6 @@ Xterm.js is used in many world-class applications to provide great terminal expe
- [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators. - [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators.
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders. - [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
- [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer. - [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer.
- [**Cate**](https://github.com/0-AI-UG/cate): Open source desktop IDE on an infinite zoomable canvas. Editor, browser, and terminal panels float in spatial space; the terminals are xterm.js with the WebGL addon, backed by node-pty.
- [**OmnyShell PWA**](https://github.com/OmnyGrid/omnyshell_web): Web client for [**OmnyShell**](https://github.com/OmnyGrid/omnyshell), featuring remote terminals, a built-in IDE, AI-assisted workflows, and detachable/resumable sessions for seamless browser-based access to nodes.
- [**kmux**](https://github.com/kkd927/kmux): A keyboard-centric terminal workspace for running parallel AI coding agents side by side on macOS and Linux.
- [**LynxHub**](https://github.com/KindaBrazy/LynxHub): Cross-platform, extensible terminal and browser for AI management.
- [**Codeman**](https://github.com/Ark0N/Codeman): Self-hosted mission control for AI coding agents, streaming live tmux sessions to browser and phone.
- [**JSVIsion**](https://github.com/blendsdk/jsvision): A Turbo Vision-Style TUI SDK for TypeScript. We use xterm.js in our docs to show live JSVision examples running in a web terminal.
- [**BitFun**](https://github.com/GCWing/BitFun): Open-source desktop AI agent whose integrated terminals and read-only terminal output views use xterm.js with the Fit, Web Links, and WebGL addons.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D) - [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. Please add any new contributions to the end of the list. 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. Please add any new contributions to the end of the list.
+1 -8
View File
@@ -98,7 +98,7 @@ terminal.loadAddon(imageAddon);
Set by default, change it with `{iipSupport: true}`. Set by default, change it with `{iipSupport: true}`.
The IIP implementation has the following features / restrictions (sequence will silently fail for unmet conditions): The IIP implementation has the following features / restrictions (sequence will silently fail for unmet conditions):
- Supported formats: PNG, JPEG, GIF, QOI, WEBP and AVIF - Supported formats: PNG, JPEG, GIF and QOI
- No animation support. - No animation support.
- Image type hinting is not supported (always deducted from data header). - Image type hinting is not supported (always deducted from data header).
- File download is not supported. - File download is not supported.
@@ -112,13 +112,6 @@ terminal.loadAddon(imageAddon);
- ReportCellSize sequence is supported. - ReportCellSize sequence is supported.
- Hi-res display support is currently limited (image gets blurred). - Hi-res display support is currently limited (image gets blurred).
- **Note on IIP image format support**
For all image formats except QOI the addon only does the dimension extraction and leaves
the image decoding to the underlying browser engine. Note that some formats are implemented
and enabled with different constraints by browser vendors, especially newer formats
(e.g. under Firefox you might need to change the setting _image.avif.compliance_strictness_ to 0
to get broader AVIF support).
- **Kitty Graphics Support (TGP)** - **Kitty Graphics Support (TGP)**
Set by default, change it with `{kittySupport: true}`. Set by default, change it with `{kittySupport: true}`.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

+41 -49
View File
@@ -3,13 +3,13 @@
* @license MIT * @license MIT
*/ */
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types'; import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
import { ImageRenderer } from './ImageRenderer';
import { IIPImageStorage } from './IIPImageStorage'; import { IIPImageStorage } from './IIPImageStorage';
import { CELL_SIZE_DEFAULT } from './ImageStorage'; import { CELL_SIZE_DEFAULT } from './ImageStorage';
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm'; import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm'; import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser'; import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './Metrics'; import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
import { Drawable } from './Primitives';
// Local const enum mirror - esbuild can't inline const enums from external packages // Local const enum mirror - esbuild can't inline const enums from external packages
const enum DecoderConst { const enum DecoderConst {
@@ -39,11 +39,13 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private _header: IHeaderFields = DEFAULT_HEADER; private _header: IHeaderFields = DEFAULT_HEADER;
private _dec: Base64Decoder; private _dec: Base64Decoder;
private _qoiDec: QoiDecoder; private _qoiDec: QoiDecoder;
private _metrics = UNSUPPORTED_TYPE;
private _isMultipart = false; private _isMultipart = false;
private _abortMulti = false; private _abortMulti = false;
constructor( constructor(
private readonly _opts: IImageAddonOptions, private readonly _opts: IImageAddonOptions,
private readonly _renderer: ImageRenderer,
private readonly _storage: IIPImageStorage, private readonly _storage: IIPImageStorage,
private readonly _coreTerminal: ITerminalExt private readonly _coreTerminal: ITerminalExt
) { ) {
@@ -61,6 +63,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
public start(): void { public start(): void {
this._aborted = false; this._aborted = false;
this._metrics = UNSUPPORTED_TYPE;
this._hp.reset(); this._hp.reset();
} }
@@ -117,16 +120,14 @@ export class IIPHandler implements IOscHandler, IResetHandler {
if (seqType === SequenceType.REPORTCELLSIZE) { if (seqType === SequenceType.REPORTCELLSIZE) {
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST // OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
// IMPORTANT: ReportCellSize uses logical points (CSS pixels) let width = CELL_SIZE_DEFAULT.width;
let w = CELL_SIZE_DEFAULT.width; let height = CELL_SIZE_DEFAULT.height;
let h = CELL_SIZE_DEFAULT.height; if (this._renderer.dimensions) {
const dimensions = this._coreTerminal.dimensions; width = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
if (dimensions) { height = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
w = dimensions.css.canvas.width / this._coreTerminal.cols;
h = dimensions.css.canvas.height / this._coreTerminal.rows;
} }
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1; const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
const report = `\x1b]1337;ReportCellSize=${h.toFixed(3)};${w.toFixed(3)};${scale.toFixed(3)}\x1b\\`; const report = `\x1b]1337;ReportCellSize=${height.toFixed(3)};${width.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
this._coreTerminal.input(report, false); this._coreTerminal.input(report, false);
return true; return true;
} }
@@ -148,21 +149,22 @@ export class IIPHandler implements IOscHandler, IResetHandler {
// fallthrough for SequenceType.FILE & SequenceType.FILEEND // fallthrough for SequenceType.FILE & SequenceType.FILEEND
let w = 0;
let h = 0;
// early exit condition chain // early exit condition chain
let cond: number | boolean; let cond: number | boolean;
let metrics = UNSUPPORTED_TYPE;
if (cond = success) { if (cond = success) {
if (cond = !this._dec.end()) { if (cond = !this._dec.end()) {
metrics = imageType(this._dec.data8); this._metrics = imageType(this._dec.data8);
if (cond = metrics.mime !== 'unsupported') { if (cond = this._metrics.mime !== 'unsupported') {
if (!(cond = metrics.width && metrics.height && metrics.width * metrics.height < this._opts.pixelLimit)) { w = this._metrics.width;
console.warn(`IIP: image dimension issue ${metrics.width}x${metrics.height}`); h = this._metrics.height;
if (cond = w && h && w * h < this._opts.pixelLimit) {
[w, h] = this._resize(w, h).map(Math.floor);
cond = w && h && w * h < this._opts.pixelLimit;
} }
} else {
console.warn('IIP: unsupported image type');
} }
} else {
console.warn('IIP: error during BASE64 decoding');
} }
} }
if (!cond) { if (!cond) {
@@ -170,52 +172,42 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return true; return true;
} }
let bmSrc: Blob | ImageData; let blob: Blob | ImageData;
let imgBlob: Blob; if (this._metrics.mime === 'image/qoi') {
if (metrics.mime === 'image/qoi') {
const data = this._qoiDec.decode(this._dec.data8); const data = this._qoiDec.decode(this._dec.data8);
bmSrc = new ImageData( blob = new ImageData(
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength), new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
this._qoiDec.width, this._qoiDec.width,
this._qoiDec.height this._qoiDec.height
); );
this._qoiDec.release(); this._qoiDec.release();
if (w === this._qoiDec.width && h === this._qoiDec.height) {
// use fast-path if we don't need to rescale
this._dec.release();
const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
canvas.getContext('2d')?.putImageData(blob, 0, 0);
this._storage.addImage(canvas);
return true;
}
} else { } else {
imgBlob = bmSrc = new Blob([this._dec.data8], { type: metrics.mime }); blob = new Blob([this._dec.data8], { type: this._metrics.mime });
} }
this._dec.release(); this._dec.release();
return createImageBitmap(bmSrc) return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
.then(bm => { .then(bm => {
const [w, h] = this._resize(metrics.width, metrics.height); this._storage.addImage(bm);
this._storage.addImage(new Drawable(bm), imgBlob, metrics, w / metrics.width, h / metrics.height);
return true;
})
.catch(e => {
console.warn(`IIP: decoding error ${metrics.mime} ${metrics.width}x${metrics.height}`, e);
return true; return true;
}); });
} }
private _resize(w: number, h: number): [number, number] { private _resize(w: number, h: number): [number, number] {
let cw; const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
let ch; const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
let width; const width = this._renderer.dimensions?.css.canvas.width || cw * this._coreTerminal.cols;
let height; const height = this._renderer.dimensions?.css.canvas.height || ch * this._coreTerminal.rows;
const dimensions = this._coreTerminal.dimensions;
if (dimensions) {
width = dimensions.device.canvas.width;
height = dimensions.device.canvas.height;
cw = width / this._coreTerminal.cols;
ch = height / this._coreTerminal.rows;
} else {
cw = CELL_SIZE_DEFAULT.width;
ch = CELL_SIZE_DEFAULT.height;
width = cw * this._coreTerminal.cols;
height = ch * this._coreTerminal.rows;
}
const rw = this._dim(this._header.width ?? '', width, cw); const rw = this._dim(this._header.width!, width, cw);
const rh = this._dim(this._header.height ?? '', height, ch); const rh = this._dim(this._header.height!, height, ch);
if (!rw && !rh) { if (!rw && !rh) {
const wf = width / w; // TODO: should this respect initial cursor offset? const wf = width / w; // TODO: should this respect initial cursor offset?
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
+3 -5
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { IAddImageOpts, IDrawable, IMetrics } from './Types'; import { IAddImageOpts } from './Types';
import { ImageStorage } from './ImageStorage'; import { ImageStorage } from './ImageStorage';
/** /**
@@ -22,9 +22,7 @@ export class IIPImageStorage {
* Add an IIP image to storage. * Add an IIP image to storage.
* Always uses scrolling mode — cursor advances past the image. * Always uses scrolling mode — cursor advances past the image.
*/ */
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics, prescaleX: number, prescaleY: number): void { public addImage(img: HTMLCanvasElement | ImageBitmap): void {
this._addImageOpts.prescaleX = prescaleX; this._storage.addImage(img, this._addImageOpts);
this._addImageOpts.prescaleY = prescaleY;
this._storage.addImage(src, data, metrics, this._addImageOpts);
} }
} }
@@ -4,8 +4,7 @@
*/ */
import { assert } from 'chai'; import { assert } from 'chai';
import { imageType } from './Metrics'; import { imageType, IMetrics } from './IIPMetrics';
import type { IMetrics } from 'Types';
// fix missing nodejs decl // fix missing nodejs decl
declare const require: (s: string) => any; declare const require: (s: string) => any;
@@ -33,28 +32,6 @@ const TEST_IMAGES: [string, IMetrics][] = [
['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8 }], ['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8 }],
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }], ['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }],
['dice.qoi', { mime: 'image/qoi', width: 800, height: 600 }], ['dice.qoi', { mime: 'image/qoi', width: 800, height: 600 }],
// VP8
['1.webp', { mime: 'image/webp', width: 550, height: 368 }],
['2.webp', { mime: 'image/webp', width: 550, height: 404 }],
['3.webp', { mime: 'image/webp', width: 1280, height: 720 }],
['4.webp', { mime: 'image/webp', width: 1024, height: 772 }],
['5.webp', { mime: 'image/webp', width: 1024, height: 752 }],
// VP8X
['1_webp_a.webp', { mime: 'image/webp', width: 400, height: 301 }],
['2_webp_a.webp', { mime: 'image/webp', width: 386, height: 395 }],
['3_webp_a.webp', { mime: 'image/webp', width: 800, height: 600 }],
['4_webp_a.webp', { mime: 'image/webp', width: 421, height: 163 }],
['5_webp_a.webp', { mime: 'image/webp', width: 300, height: 300 }],
// VP8L
['1_webp_ll.webp', { mime: 'image/webp', width: 400, height: 301 }],
['2_webp_ll.webp', { mime: 'image/webp', width: 386, height: 395 }],
['3_webp_ll.webp', { mime: 'image/webp', width: 800, height: 600 }],
['4_webp_ll.webp', { mime: 'image/webp', width: 421, height: 163 }],
['5_webp_ll.webp', { mime: 'image/webp', width: 300, height: 300 }],
// some AVIF test images
['fox.profile2.12bpc.yuv444.odd-width.odd-height.avif', { mime: 'image/avif', width: 1203, height: 799 }],
['hato.profile0.8bpc.yuv420.avif', { mime: 'image/avif', width: 3082, height: 2048 }],
['kimono.crop.avif', { mime: 'image/avif', width: 722, height: 1024 }],
]; ];
+84
View File
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'unsupported' | '';
export interface IMetrics {
mime: ImageType;
width: number;
height: number;
}
export const UNSUPPORTED_TYPE: IMetrics = {
mime: 'unsupported',
width: 0,
height: 0
};
export function imageType(d: Uint8Array): IMetrics {
if (d.length < 24) {
return UNSUPPORTED_TYPE;
}
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
// + first chunk must be IHDR
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
return {
mime: 'image/png',
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
};
}
// JPEG: FF D8 FF
if (d[0] === 0xFF && d[1] === 0xD8 && d[2] === 0xFF) {
const [width, height] = jpgSize(d);
return { mime: 'image/jpeg', width, height };
}
// GIF: GIF87a or GIF89a
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
return {
mime: 'image/gif',
width: d[7] << 8 | d[6],
height: d[9] << 8 | d[8]
};
}
// QOI: qoif
if (d32[0] === 0x66696F71) {
return {
mime: 'image/qoi',
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
};
}
return UNSUPPORTED_TYPE;
}
function jpgSize(d: Uint8Array): [number, number] {
const len = d.length;
let i = 4;
let blockLength = d[i] << 8 | d[i + 1];
while (true) {
i += blockLength;
if (i >= len) {
// exhausted without size info
return [0, 0];
}
if (d[i] !== 0xFF) {
return [0, 0];
}
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
if (i + 8 < len) {
return [
d[i + 7] << 8 | d[i + 8],
d[i + 5] << 8 | d[i + 6]
];
}
return [0, 0];
}
i += 2;
blockLength = d[i] << 8 | d[i + 1];
}
}
+8 -7
View File
@@ -5,7 +5,7 @@
import type { ITerminalAddon, IDisposable } from '@xterm/xterm'; import type { ITerminalAddon, IDisposable } from '@xterm/xterm';
import type { ImageAddon as IImageApi } from '@xterm/addon-image'; import type { ImageAddon as IImageApi } from '@xterm/addon-image';
import { Emitter, type IEvent } from 'common/Event'; import { Emitter, type IEvent } from 'common/primitives/Event';
import { IIPHandler } from './IIPHandler'; import { IIPHandler } from './IIPHandler';
import { ImageRenderer } from './ImageRenderer'; import { ImageRenderer } from './ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage'; import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
@@ -144,9 +144,6 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
terminal.options.windowOptions = windowOps; terminal.options.windowOptions = windowOps;
} }
// apply placeholder setting
this._renderer.showPlaceholder(this._opts.showPlaceholder);
this._disposeLater( this._disposeLater(
this._renderer, this._renderer,
this._storage, this._storage,
@@ -190,7 +187,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
// iTerm IIP handler // iTerm IIP handler
if (this._opts.iipSupport) { if (this._opts.iipSupport) {
const iipStorage = new IIPImageStorage(this._storage!); const iipStorage = new IIPImageStorage(this._storage!);
const iipHandler = new IIPHandler(this._opts, iipStorage, terminal); const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
this._handlers.set('iip', iipHandler); this._handlers.set('iip', iipHandler);
this._disposeLater( this._disposeLater(
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler) terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
@@ -253,6 +250,10 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
return this._storage?.getImageAtBufferCell(x, y); return this._storage?.getImageAtBufferCell(x, y);
} }
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
return this._storage?.extractTileAtBufferCell(x, y);
}
private _report(s: string): void { private _report(s: string): void {
this._terminal?._core.input(s, false); this._terminal?._core.input(s, false);
} }
@@ -343,8 +344,8 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
switch (params[1]) { switch (params[1]) {
// we only implement read and read_max here // we only implement read and read_max here
case GaAction.READ: case GaAction.READ:
let width = this._renderer?.dimensions?.device.canvas.width; let width = this._renderer?.dimensions?.css.canvas.width;
let height = this._renderer?.dimensions?.device.canvas.height; let height = this._renderer?.dimensions?.css.canvas.height;
if (!width || !height) { if (!width || !height) {
// for some reason we have no working image renderer // for some reason we have no working image renderer
// --> fallback to default cell size // --> fallback to default cell size
+212 -107
View File
@@ -3,14 +3,19 @@
* @license MIT * @license MIT
*/ */
import { toRGBA8888 } from 'sixel/lib/Colors';
import { IDisposable } from '@xterm/xterm'; import { IDisposable } from '@xterm/xterm';
import type { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types'; import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { Disposable, MutableDisposable, toDisposable } from 'common/primitives/Lifecycle';
import { createCanvas } from './Primitives';
const enum Constants {
PLACEHOLDER_LENGTH = 4096,
PLACEHOLDER_HEIGHT = 24
}
/** /**
* ImageRenderer - terminal frontend extension: * ImageRenderer - terminal frontend extension:
* - provide primitives for canvas, ImageData, Bitmap (static)
* - add canvas layer to DOM (browser only for now) * - add canvas layer to DOM (browser only for now)
* - draw image tiles onRender * - draw image tiles onRender
*/ */
@@ -19,14 +24,51 @@ export class ImageRenderer extends Disposable implements IDisposable {
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; } public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>(); private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
private _placeholder: HTMLCanvasElement | undefined; private _placeholder: HTMLCanvasElement | undefined;
private _placeholderBitmap: ImageBitmap | undefined;
private _optionsRefresh = this._register(new MutableDisposable()); private _optionsRefresh = this._register(new MutableDisposable());
private _oldOpen: ((parent: HTMLElement) => void) | undefined; private _oldOpen: ((parent: HTMLElement) => void) | undefined;
private _renderService: IRenderService | undefined; private _renderService: IRenderService | undefined;
private _oldSetRenderer: ((renderer: any) => void) | undefined; private _oldSetRenderer: ((renderer: any) => void) | undefined;
// some local variables for faster access // drawing primitive - canvas
private _dimensions: IRenderDimensions | undefined; public static createCanvas(localDocument: Document | undefined, width: number, height: number): HTMLCanvasElement {
private _cellSize: ICellSize | undefined; /**
* NOTE: We normally dont care, from which document the canvas
* gets created, so we can fall back to global document,
* if the terminal has no document associated yet.
* This way early image loads before calling .open keep working
* (still discouraged though, as the metrics will be screwed up).
* Only the DOM output canvas should be on the terminal's document,
* which gets explicitly checked in `insertLayerToDom`.
*/
const canvas = (localDocument ?? document).createElement('canvas');
canvas.width = width | 0;
canvas.height = height | 0;
return canvas;
}
// drawing primitive - ImageData with optional buffer
public static createImageData(ctx: CanvasRenderingContext2D, width: number, height: number, buffer?: ArrayBuffer): ImageData {
if (typeof ImageData !== 'function') {
const imgData = ctx.createImageData(width, height);
if (buffer) {
imgData.data.set(new Uint8ClampedArray(buffer, 0, width * height * 4));
}
return imgData;
}
return buffer
? new ImageData(new Uint8ClampedArray(buffer, 0, width * height * 4), width, height)
: new ImageData(width, height);
}
// drawing primitive - ImageBitmap
public static createImageBitmap(img: ImageBitmapSource): Promise<ImageBitmap | undefined> {
if (typeof createImageBitmap !== 'function') {
return Promise.resolve(undefined);
}
return createImageBitmap(img);
}
constructor(private _terminal: ITerminalExt) { constructor(private _terminal: ITerminalExt) {
super(); super();
@@ -58,6 +100,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
} }
this._renderService = undefined; this._renderService = undefined;
this._layers.clear(); this._layers.clear();
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;
this._placeholder = undefined; this._placeholder = undefined;
})); }));
} }
@@ -66,7 +110,15 @@ export class ImageRenderer extends Disposable implements IDisposable {
* Enable the placeholder. * Enable the placeholder.
*/ */
public showPlaceholder(value: boolean): void { public showPlaceholder(value: boolean): void {
this._placeholder = value ? this._createPlaceHolder() : undefined; if (value) {
if (!this._placeholder && this.cellSize.height !== -1) {
this._createPlaceHolder(Math.max(this.cellSize.height + 1, Constants.PLACEHOLDER_HEIGHT));
}
} else {
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;
this._placeholder = undefined;
}
this._renderService?.refreshRows(0, this._terminal.rows); this._renderService?.refreshRows(0, this._terminal.rows);
} }
@@ -79,27 +131,22 @@ export class ImageRenderer extends Disposable implements IDisposable {
} }
/** /**
* Current cell size. * Current cell size (float).
*/ */
public getCellSize(dimensions?: IRenderDimensions): ICellSize | undefined { public get cellSize(): ICellSize {
dimensions ??= this._terminal.dimensions; return {
if (dimensions) { width: this.dimensions?.css.cell.width || -1,
return { height: this.dimensions?.css.cell.height || -1
width: dimensions.device.canvas.width / this._terminal.cols, };
height: dimensions.device.canvas.height / this._terminal.rows,
};
}
} }
/** /**
* Clear a region of the image layer canvas. * Clear a region of the image layer canvas.
*/ */
public clearLines(start: number, end: number, layer?: ImageLayer): void { public clearLines(start: number, end: number, layer?: ImageLayer): void {
const deviceGrid = this._cellSize; const y = start * (this.dimensions?.css.cell.height || 0);
if (!deviceGrid || !this._dimensions) return; const w = this.dimensions?.css.canvas.width || 0;
const y = Math.floor(start * deviceGrid.height); const h = (end + 1 - start) * (this.dimensions?.css.cell.height || 0);
const w = this._dimensions.device.canvas.width;
const h = Math.ceil((end + 1 - start) * deviceGrid.height);
if (!layer || layer === 'top') { if (!layer || layer === 'top') {
this._layers.get('top')?.clearRect(0, y, w, h); this._layers.get('top')?.clearRect(0, y, w, h);
} }
@@ -127,103 +174,142 @@ export class ImageRenderer extends Disposable implements IDisposable {
*/ */
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void { public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
const ctx = this._layers.get(imgSpec.layer); const ctx = this._layers.get(imgSpec.layer);
const initialGrid = imgSpec.cellSize; if (!ctx) {
const deviceGrid = this._cellSize; return;
}
const { width, height } = this.cellSize;
if (!ctx || !imgSpec.src.native || !deviceGrid) { // Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return; return;
} }
const img = imgSpec.src; this._rescaleImage(imgSpec, width, height);
const cols = Math.ceil(img.width * imgSpec.prescaleX / initialGrid.width); const img = imgSpec.actual!;
const cols = Math.ceil(img.width / width);
const gridWidth = initialGrid.width / imgSpec.prescaleX; const sx = (tileId % cols) * width;
const gridHeight = initialGrid.height / imgSpec.prescaleY; const sy = Math.floor(tileId / cols) * height;
const dx = col * width;
const dy = row * height;
// crops are initialGrid px // safari bug: never access image source out of bounds
const sx = (tileId % cols) * gridWidth + imgSpec.precropX; const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
const sy = Math.floor(tileId / cols) * gridHeight + imgSpec.precropY; const finalHeight = sy + height > img.height ? img.height - sy : height;
const dx = Math.floor(col * deviceGrid.width + imgSpec.offsetX);
const dy = Math.floor(row * deviceGrid.height + imgSpec.offsetY);
// safari bug: never access image source out of bounds, thus we clamp its dimensions
const sWidth = Math.min(count * gridWidth, img.width - sx);
const sHeight = Math.min(gridHeight, img.height - sy);
const dWidth = Math.ceil(sWidth / gridWidth * deviceGrid.width * imgSpec.scaleX);
const dHeight = Math.ceil(sHeight / gridHeight * deviceGrid.height * imgSpec.scaleY);
if (imgSpec.blendMode === 'overwrite') {
ctx.clearRect(dx, dy, dWidth, dHeight);
}
// Floor all pixel offsets to get stable tile mapping without any overflows. // Floor all pixel offsets to get stable tile mapping without any overflows.
// Note: For not pixel perfect aligned cells like in the DOM renderer // Note: For not pixel perfect aligned cells like in the DOM renderer
// this will move a tile slightly to the top/left (subpixel range, thus ignore it). // this will move a tile slightly to the top/left (subpixel range, thus ignore it).
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width // FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
ctx.drawImage( ctx.drawImage(
img.native, img,
Math.floor(sx), Math.floor(sy), Math.ceil(sWidth), Math.ceil(sHeight), Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
dx, dy, dWidth, dHeight Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
); );
} }
/**
* Extract a single tile from an image.
*/
public extractTile(imgSpec: IImageSpec, tileId: number): HTMLCanvasElement | undefined {
const { width, height } = this.cellSize;
// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return;
}
this._rescaleImage(imgSpec, width, height);
const img = imgSpec.actual!;
const cols = Math.ceil(img.width / width);
const sx = (tileId % cols) * width;
const sy = Math.floor(tileId / cols) * height;
const finalWidth = width + sx > img.width ? img.width - sx : width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
);
return canvas;
}
}
/** /**
* Draw a line with placeholder on the image layer canvas. * Draw a line with placeholder on the image layer canvas.
*/ */
public drawPlaceholder(col: number, row: number, count: number = 1): void { public drawPlaceholder(col: number, row: number, count: number = 1): void {
const ctx = this._layers.get('top'); const ctx = this._layers.get('top');
const deviceGrid = this._cellSize; if (ctx) {
if (!ctx || !deviceGrid) { const { width, height } = this.cellSize;
return;
// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return;
}
if (!this._placeholder) {
this._createPlaceHolder(Math.max(height + 1, Constants.PLACEHOLDER_HEIGHT));
} else if (height >= this._placeholder!.height) {
this._createPlaceHolder(height + 1);
}
if (!this._placeholder) return;
ctx.drawImage(
this._placeholderBitmap ?? this._placeholder!,
col * width,
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
width * count,
height,
col * width,
row * height,
width * count,
height
);
} }
this._placeholder ??= this._createPlaceHolder();
if (!this._placeholder) return;
ctx.drawImage(
this._placeholder,
0, 0, 1, 1,
col * deviceGrid.width,
row * deviceGrid.height,
count * deviceGrid.width,
deviceGrid.height
);
} }
/** /**
* Rescale image layer canvas if needed. * Rescale image layer canvas if needed.
* Checked once from `ImageStorage.render`. * Checked once from `ImageStorage.render`.
* NOTE: This method updates dimensions on instance properties
* for faster access during draw and clear calls.
* So make sure to always call this before doing any draws.
*/ */
public rescaleCanvas(force: boolean = false): void { public rescaleCanvas(): void {
const dimensions = this._terminal.dimensions; const w = this.dimensions?.css.canvas.width || 0;
if (dimensions) { const h = this.dimensions?.css.canvas.height || 0;
const cssW = dimensions.css.canvas.width; for (const ctx of this._layers.values()) {
const cssH = dimensions.css.canvas.height; if (ctx.canvas.width !== w || ctx.canvas.height !== h) {
const devW = dimensions.device.canvas.width; ctx.canvas.width = w;
const devH = dimensions.device.canvas.height; ctx.canvas.height = h;
let recalc = force;
for (const ctx of this._layers.values()) {
if (ctx.canvas.width !== devW
|| ctx.canvas.height !== devH
|| ctx.canvas.style.width !== `${cssW}px`
|| ctx.canvas.style.height !== `${cssH}px`
) {
ctx.canvas.width = devW;
ctx.canvas.height = devH;
ctx.canvas.style.width = `${cssW}px`;
ctx.canvas.style.height = `${cssH}px`;
recalc = true;
}
} }
if (recalc) { }
this._dimensions = dimensions; }
this._cellSize = this.getCellSize(dimensions);
} /**
} else { * Rescale image in storage if needed.
// we were not able to get render dimensions: */
// set cellsize to undefined so we don't try to draw anything private _rescaleImage(spec: IImageSpec, currentWidth: number, currentHeight: number): void {
this._cellSize = this.getCellSize(); if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
return;
}
const { width: originalWidth, height: originalHeight } = spec.origCellSize;
if (currentWidth === originalWidth && currentHeight === originalHeight) {
spec.actual = spec.orig;
spec.actualCellSize.width = originalWidth;
spec.actualCellSize.height = originalHeight;
return;
}
const canvas = ImageRenderer.createCanvas(
this.document,
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(spec.orig!, 0, 0, canvas.width, canvas.height);
spec.actual = canvas;
spec.actualCellSize.width = currentWidth;
spec.actualCellSize.height = currentHeight;
} }
} }
@@ -250,12 +336,9 @@ export class ImageRenderer extends Disposable implements IDisposable {
if (this._layers.has(layer)) { if (this._layers.has(layer)) {
return; return;
} }
const canvas = createCanvas( const canvas = ImageRenderer.createCanvas(
this.document, this.document, this.dimensions?.css.canvas.width || 0,
this.dimensions?.device.canvas.width || 0, this.dimensions?.css.canvas.height || 0
this.dimensions?.device.canvas.height || 0,
this.dimensions?.css.canvas.width || 0,
this.dimensions?.css.canvas.height || 0,
); );
canvas.classList.add(`xterm-image-layer-${layer}`); canvas.classList.add(`xterm-image-layer-${layer}`);
const screenElement = this._terminal._core.screenElement; const screenElement = this._terminal._core.screenElement;
@@ -283,8 +366,6 @@ export class ImageRenderer extends Disposable implements IDisposable {
return; return;
} }
this._layers.set(layer, ctx); this._layers.set(layer, ctx);
// force rescaling to update stored cellSize (maybe not be populated yet)
this.rescaleCanvas(true);
this.clearAll(layer); this.clearAll(layer);
} }
@@ -300,17 +381,41 @@ export class ImageRenderer extends Disposable implements IDisposable {
return this._layers.has(layer); return this._layers.has(layer);
} }
/** private _createPlaceHolder(height: number = Constants.PLACEHOLDER_HEIGHT): void {
* Create a semi-transparent gray 1x1 placeholder this._placeholderBitmap?.close();
*/ this._placeholderBitmap = undefined;
private _createPlaceHolder(): HTMLCanvasElement | undefined {
const imgData = new ImageData(1, 1); // create blueprint to fill placeholder with
new Uint32Array(imgData.data.buffer).fill(128 << 24 | 128 << 16 | 128 << 8 | 128); const bWidth = 32; // must be 2^n
const canvas = createCanvas(this.document, 1, 1); const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
const ctx = canvas.getContext('2d', { alpha: true }); const ctx = blueprint.getContext('2d', { alpha: false });
if (!ctx) return; if (!ctx) return;
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
const d32 = new Uint32Array(imgData.data.buffer);
const black = toRGBA8888(0, 0, 0);
const white = toRGBA8888(255, 255, 255);
d32.fill(black);
for (let y = 0; y < height; ++y) {
const shift = y % 2;
const offset = y * bWidth;
for (let x = 0; x < bWidth; x += 2) {
d32[offset + x + shift] = white;
}
}
ctx.putImageData(imgData, 0, 0); ctx.putImageData(imgData, 0, 0);
return canvas;
// create placeholder line, width aligned to blueprint width
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || Constants.PLACEHOLDER_LENGTH;
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
if (!ctx2) {
this._placeholder = undefined;
return;
}
for (let i = 0; i < width; i += bWidth) {
ctx2.drawImage(blueprint, i, 0);
}
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
} }
public get document(): Document | undefined { public get document(): Document | undefined {
+232 -140
View File
@@ -5,14 +5,12 @@
import { IDisposable } from '@xterm/xterm'; import { IDisposable } from '@xterm/xterm';
import { ImageRenderer } from './ImageRenderer'; import { ImageRenderer } from './ImageRenderer';
import type { import {
ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, IAddImageOpts, ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec,
IDrawable, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes,
IMetrics UnderlineStyle, IAddImageOpts
} from './Types'; } from './Types';
import type { IBufferLine } from 'common/buffer/Types';
import { CellData } from 'common/buffer/CellData';
import { createCanvas } from './Primitives';
// fallback default cell size // fallback default cell size
export const CELL_SIZE_DEFAULT: ICellSize = { export const CELL_SIZE_DEFAULT: ICellSize = {
@@ -20,12 +18,93 @@ export const CELL_SIZE_DEFAULT: ICellSize = {
height: 14 height: 14
}; };
class ImageTileInfo { /**
* Extend extended attribute to also hold image tile information.
*
* Object definition is copied from base repo to fully mimick its behavior.
* Image data is added as additional public properties `imageId` and `tileId`.
*/
class ExtendedAttrsImage implements IExtendedAttrsImage {
private _ext: number = 0;
public get ext(): number {
if (this._urlId) {
return (
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
(this.underlineStyle << 26)
);
}
return this._ext;
}
public set ext(value: number) { this._ext = value; }
public get underlineStyle(): UnderlineStyle {
// Always return the URL style if it has one
if (this._urlId) {
return UnderlineStyle.DASHED;
}
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
}
public set underlineStyle(value: UnderlineStyle) {
this._ext &= ~ExtFlags.UNDERLINE_STYLE;
this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;
}
public get underlineColor(): number {
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
public set underlineColor(value: number) {
this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
public get underlineVariantOffset(): number {
const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;
if (val < 0) {
return val ^ 0xFFFFFFF8;
}
return val;
}
public set underlineVariantOffset(value: number) {
this._ext &= ~ExtFlags.VARIANT_OFFSET;
this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;
}
private _urlId: number = 0;
public get urlId(): number {
return this._urlId;
}
public set urlId(value: number) {
this._urlId = value;
}
constructor( constructor(
ext: number = 0,
urlId: number = 0,
public imageId = -1, public imageId = -1,
public tileId = -1) { public tileId = -1
) {
this._ext = ext;
this._urlId = urlId;
}
public clone(): IExtendedAttrsImage {
/**
* Technically we dont need a clone variant of ExtendedAttrsImage,
* as we never clone a cell holding image data.
* Note: Clone is only meant to be used by the InputHandler for
* sticky attributes, which is never the case for image data.
* We still provide a proper clone method to reflect the full ext attr
* state in case there are future use cases for clone.
*/
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
}
public isEmpty(): boolean {
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1;
} }
} }
const EMPTY_ATTRS = new ExtendedAttrsImage();
/** /**
* ImageStorage - extension of CoreTerminal: * ImageStorage - extension of CoreTerminal:
@@ -46,8 +125,7 @@ export class ImageStorage implements IDisposable {
// whether render should do a full clear // whether render should do a full clear
private _needsFullClear = false; private _needsFullClear = false;
// hard limit of stored pixels (fallback limit of 10 MB) // hard limit of stored pixels (fallback limit of 10 MB)
private _byteLimit: number = 10000000; private _pixelLimit: number = 2500000;
private _workCell: CellData = new CellData();
private _viewportMetrics: { cols: number, rows: number }; private _viewportMetrics: { cols: number, rows: number };
public onImageAdded: (() => void) | undefined; public onImageAdded: (() => void) | undefined;
@@ -86,54 +164,43 @@ export class ImageStorage implements IDisposable {
this._renderer.clearAll(); this._renderer.clearAll();
} }
/**
* FIXME: below we need to introduce separation of:
* - RAM usage
* - VRAM usage
* - Blob unsage
*/
public getLimit(): number { public getLimit(): number {
return this._byteLimit / 1000000; return this._pixelLimit * 4 / 1000000;
} }
public setLimit(value: number): void { public setLimit(value: number): void {
if (value < 0.5 || value > 1000) { if (value < 0.5 || value > 1000) {
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1GB'); throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
} }
this._byteLimit = (value * 1000000) >>> 0; this._pixelLimit = (value / 4 * 1000000) >>> 0;
this._evictOldest(0); this._evictOldest(0);
} }
public getUsage(): number { public getUsage(): number {
return this._getVRAMUsage() / 1000000; return this._getStoredPixels() * 4 / 1000000;
} }
private _getVRAMUsage(): number { private _getStoredPixels(): number {
let bytes = 0; let storedPixels = 0;
for (const spec of this._images.values()) { for (const spec of this._images.values()) {
bytes += spec.src.bytes; if (spec.orig) {
} storedPixels += spec.orig.width * spec.orig.height;
return bytes; if (spec.actual && spec.actual !== spec.orig) {
} storedPixels += spec.actual.width * spec.actual.height;
}
public getBlobUsage(): number {
let size = 0;
for (const spec of this._images.values()) {
if (spec.data instanceof Blob) {
size += spec.data.size;
} }
} }
return size; return storedPixels;
} }
private _delImg(id: number): void { private _delImg(id: number): void {
const spec = this._images.get(id); const spec = this._images.get(id);
if (!spec) return; if (!spec) return;
this._images.delete(id); this._images.delete(id);
spec.src.close(); // FIXME: really ugly workaround to get bitmaps deallocated :(
spec.data = undefined; if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
spec.orig.close();
}
this.onImageDeleted?.(id); this.onImageDeleted?.(id);
} }
@@ -170,10 +237,8 @@ export class ImageStorage implements IDisposable {
} }
/** /**
* Method to add an drawable to the storage. * Method to add an image to the storage.
* @param src - The drawable to add. * @param img - The image to add (canvas or bitmap).
* @param data - Blob or Uint8Array of the original image data.
* @param metrics - IMetrics of the original image.
* @param opts - Options for addImage: * @param opts - Options for addImage:
* - scrolling: When true, cursor advances with the image. * - scrolling: When true, cursor advances with the image.
* When false, image is placed at ORIGIN and cursor does not move. * When false, image is placed at ORIGIN and cursor does not move.
@@ -182,16 +247,17 @@ export class ImageStorage implements IDisposable {
* - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right. * - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
* @returns The internal image ID assigned to the stored image. * @returns The internal image ID assigned to the stored image.
*/ */
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics, opts: IAddImageOpts): number { public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
// never allow storage to exceed memory limit // never allow storage to exceed memory limit
this._evictOldest(src.bytes); this._evictOldest(img.width * img.height);
// calc rows x cols needed to display the image // calc rows x cols needed to display the image
const prescaleX = opts.prescaleX ?? 1.0; let cellSize = this._renderer.cellSize;
const prescaleY = opts.prescaleY ?? 1.0; if (cellSize.width === -1 || cellSize.height === -1) {
const cellSize = this._renderer.getCellSize() ?? CELL_SIZE_DEFAULT; cellSize = CELL_SIZE_DEFAULT;
const cols = Math.ceil(src.width * prescaleX / cellSize.width); }
const rows = Math.ceil(src.height * prescaleY / cellSize.height); const cols = Math.ceil(img.width / cellSize.width);
const rows = Math.ceil(img.height / cellSize.height);
const imageId = ++this._lastId; const imageId = ++this._lastId;
@@ -211,10 +277,10 @@ export class ImageStorage implements IDisposable {
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y); this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
for (let row = 0; row < rows; ++row) { for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(buffer.y + buffer.ybase)!; const line = buffer.lines.get(buffer.y + buffer.ybase);
for (let col = 0; col < cols; ++col) { for (let col = 0; col < cols; ++col) {
if (offset + col >= termCols) break; if (offset + col >= termCols) break;
this._writeToCell(line, offset + col, imageId, row * cols + col); this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
tileCount++; tileCount++;
} }
if (opts.scrolling) { if (opts.scrolling) {
@@ -268,24 +334,15 @@ export class ImageStorage implements IDisposable {
// create storage entry // create storage entry
const imgSpec: IImageSpec = { const imgSpec: IImageSpec = {
src, orig: img,
data, origCellSize: cellSize,
metrics, actual: img,
cellSize, actualCellSize: { ...cellSize }, // clone needed, since later modified
marker: endMarker || undefined, marker: endMarker || undefined,
tileCount, tileCount,
bufferType: this._terminal.buffer.active.type, bufferType: this._terminal.buffer.active.type,
layer: opts.layer, layer: opts.layer,
zIndex: opts.zIndex, zIndex: opts.zIndex
prescaleX,
prescaleY,
precropX: 0,
precropY: 0,
scaleX: 1.0,
scaleY: 1.0,
offsetX: 0,
offsetY: 0,
blendMode: 'alpha'
}; };
// finally add the image // finally add the image
@@ -370,42 +427,55 @@ export class ImageStorage implements IDisposable {
const placeholderCalls: { col: number, row: number, count: number }[] = []; const placeholderCalls: { col: number, row: number, count: number }[] = [];
// walk all cells in viewport and collect tiles found // walk all cells in viewport and collect tiles found
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
// because text writes clear the BG flag but leave image tile data intact.
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
for (let row = start; row <= end; ++row) { for (let row = start; row <= end; ++row) {
const line = buffer.lines.get(row + buffer.ydisp); const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
if (!line) return; if (!line) return;
for (let col = 0; col < cols; ++col) { for (let col = 0; col < cols; ++col) {
const e = line.getExtended(col)?.payload; let e: IExtendedAttrsImage;
if (e instanceof ImageTileInfo) { if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
const imageId = e.imageId; e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
if (imageId === undefined || imageId === -1) { } else {
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
continue; continue;
} }
const imgSpec = this._images.get(imageId); e = maybeImg;
if (e.tileId !== -1) { }
const startTile = e.tileId; const imageId = e.imageId;
const startCol = col; if (imageId === undefined || imageId === -1) {
let count = 1; continue;
/** }
* merge tiles to the right into a single draw call, if: const imgSpec = this._images.get(imageId);
* - not at end of line if (e.tileId !== -1) {
* - cell has same image id const startTile = e.tileId;
* - cell has consecutive tile id const startCol = col;
*/ let count = 1;
while (++col < cols) { /**
const nextE = line.getExtended(col)?.payload; * merge tiles to the right into a single draw call, if:
if (!(nextE instanceof ImageTileInfo) || nextE.imageId !== imageId || nextE.tileId !== startTile + count) { * - not at end of line
break; * - cell has same image id
} * - cell has consecutive tile id
count++; * Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
*/
while (++col < cols) {
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
break;
} }
col--; count++;
if (imgSpec) {
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
} else if (this._opts.showPlaceholder) {
placeholderCalls.push({ col: startCol, row, count });
}
this._fullyCleared = false;
} }
col--;
if (imgSpec) {
if (imgSpec.actual) {
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
}
} else if (this._opts.showPlaceholder) {
placeholderCalls.push({ col: startCol, row, count });
}
this._fullyCleared = false;
} }
} }
} }
@@ -443,9 +513,9 @@ export class ImageStorage implements IDisposable {
const rows = buffer.lines.length; const rows = buffer.lines.length;
const oldCol = this._viewportMetrics.cols - 1; const oldCol = this._viewportMetrics.cols - 1;
for (let row = 0; row < rows; ++row) { for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(row)!; const line = buffer.lines.get(row) as IBufferLineExt;
const e = line.getExtended(oldCol)?.payload; if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
if (e instanceof ImageTileInfo) { const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
const imageId = e.imageId; const imageId = e.imageId;
if (imageId === undefined || imageId === -1) { if (imageId === undefined || imageId === -1) {
continue; continue;
@@ -455,14 +525,14 @@ export class ImageStorage implements IDisposable {
continue; continue;
} }
// found an image tile at oldCol, check if it qualifies for right exapansion // found an image tile at oldCol, check if it qualifies for right exapansion
const tilesPerRow = Math.ceil(imgSpec.src.width * imgSpec.prescaleX / imgSpec.cellSize.width); const tilesPerRow = Math.ceil((imgSpec.actual?.width || 0) / imgSpec.actualCellSize.width);
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) { if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
continue; continue;
} }
// expand only if right side is empty (nothing got wrapped from below) // expand only if right side is empty (nothing got wrapped from below)
let hasData = false; let hasData = false;
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) { for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
if (line.hasContent(rightCol)) { if (line._data[rightCol * Cell.SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) {
hasData = true; hasData = true;
break; break;
} }
@@ -474,7 +544,7 @@ export class ImageStorage implements IDisposable {
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol); const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
let lastTile = e.tileId; let lastTile = e.tileId;
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) { for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
this._writeToCell(line, expandCol, imageId, ++lastTile); this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile);
imgSpec.tileCount++; imgSpec.tileCount++;
} }
} }
@@ -488,17 +558,34 @@ export class ImageStorage implements IDisposable {
*/ */
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined { public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
const buffer = this._terminal._core.buffer; const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y); const line = buffer.lines.get(y) as IBufferLineExt;
if (line) { if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e = line.getExtended(x)?.payload; const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) { if (e.imageId && e.imageId !== -1) {
const src = this._images.get(e.imageId)?.src; const orig = this._images.get(e.imageId)?.orig;
if (src?.native instanceof ImageBitmap || src?.native instanceof VideoFrame) { if (window.ImageBitmap && orig instanceof ImageBitmap) {
const canvas = createCanvas(window.document, src.width, src.height); const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
canvas.getContext('2d')?.drawImage(src.native, 0, 0, src.width, src.height); canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
return canvas; return canvas;
} }
return src?.native; return orig as HTMLCanvasElement;
}
}
}
/**
* Extract active single tile at buffer position.
*/
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const spec = this._images.get(e.imageId);
if (spec) {
return this._renderer.extractTile(spec, e.tileId);
}
} }
} }
} }
@@ -506,12 +593,15 @@ export class ImageStorage implements IDisposable {
// TODO: Do we need some blob offloading tricks here to avoid early eviction? // TODO: Do we need some blob offloading tricks here to avoid early eviction?
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size // also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
private _evictOldest(room: number): number { private _evictOldest(room: number): number {
const used = this._getVRAMUsage(); const used = this._getStoredPixels();
let current = used; let current = used;
while (this._byteLimit < current + room && this._images.size) { while (this._pixelLimit < current + room && this._images.size) {
const spec = this._images.get(++this._lowestId); const spec = this._images.get(++this._lowestId);
if (spec) { if (spec && spec.orig) {
current -= spec.src.bytes; current -= spec.orig.width * spec.orig.height;
if (spec.actual && spec.orig !== spec.actual) {
current -= spec.actual.width * spec.actual.height;
}
spec.marker?.dispose(); spec.marker?.dispose();
this._delImg(this._lowestId); this._delImg(this._lowestId);
} }
@@ -519,31 +609,31 @@ export class ImageStorage implements IDisposable {
return used - current; return used - current;
} }
private _writeToCell(line: IBufferLine, x: number, imageId: number, tileId: number): void { private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
const workCell = this._workCell; if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
line.loadCell(x, workCell); const old = line._extendedAttrs[x];
if (this._workCell.hasExtendedAttrs()) { if (old) {
const old = workCell.extended.payload; if (old.imageId !== undefined) {
if (old instanceof ImageTileInfo) { // found an old ExtendedAttrsImage, since we know that
// found an old ExtendedAttrsImage, since we know that // they are always isolated instances (single cell usage),
// they are always isolated instances (single cell usage), // we can re-use it and just update their id entries
// we can re-use it and just update their id entries const oldSpec = this._images.get(old.imageId);
const oldSpec = this._images.get(old.imageId); if (oldSpec) {
if (oldSpec) { // early eviction for in-viewport overwrites
// early eviction for in-viewport overwrites oldSpec.tileCount--;
oldSpec.tileCount--; }
old.imageId = imageId;
old.tileId = tileId;
return;
} }
old.imageId = imageId; // found a plain ExtendedAttrs instance, clone it to new entry
old.tileId = tileId; line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
return; return;
} }
} }
// fall-through: always create new ExtendedAttrs entry // fall-through: always create new ExtendedAttrsImage entry
const extattr = workCell.extended.clone(); line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
extattr.payload = new ImageTileInfo(imageId, tileId); line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
workCell.extended = extattr;
workCell.updateExtended();
line.setCell(x, workCell);
} }
private _evictOnAlternate(): void { private _evictOnAlternate(): void {
@@ -556,16 +646,18 @@ export class ImageStorage implements IDisposable {
// re-count tiles on whole buffer // re-count tiles on whole buffer
const buffer = this._terminal._core.buffer; const buffer = this._terminal._core.buffer;
for (let y = 0; y < this._terminal.rows; ++y) { for (let y = 0; y < this._terminal.rows; ++y) {
const line = buffer.lines.get(y); const line = buffer.lines.get(y) as IBufferLineExt;
if (!line) { if (!line) {
continue; continue;
} }
for (let x = 0; x < this._terminal.cols; ++x) { for (let x = 0; x < this._terminal.cols; ++x) {
const payload = line.getExtended(x)?.payload; if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
if (payload instanceof ImageTileInfo) { const imgId = line._extendedAttrs[x]?.imageId;
const spec = this._images.get(payload.imageId); if (imgId) {
if (spec) { const spec = this._images.get(imgId);
spec.tileCount++; if (spec) {
spec.tileCount++;
}
} }
} }
} }
-137
View File
@@ -1,137 +0,0 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IMetrics } from './Types';
export const UNSUPPORTED_TYPE: IMetrics = {
mime: 'unsupported',
width: 0,
height: 0
};
export function imageType(d: Uint8Array): IMetrics {
if (d.length < 32) {
return UNSUPPORTED_TYPE;
}
const d32 = new Uint32Array(d.buffer, d.byteOffset, 8);
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
// + first chunk must be IHDR
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
return {
mime: 'image/png',
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
};
}
// JPEG: FF D8 FF
if (d[0] === 0xFF && d[1] === 0xD8 && d[2] === 0xFF) {
const [width, height] = jpgSize(d);
return { mime: 'image/jpeg', width, height };
}
// GIF: GIF87a or GIF89a
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
return {
mime: 'image/gif',
width: d[7] << 8 | d[6],
height: d[9] << 8 | d[8]
};
}
// QOI: qoif
if (d32[0] === 0x66696F71) {
return {
mime: 'image/qoi',
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
};
}
// WEBP: RIFF | xxxx | WEBP | VP8x
if (d32[0] === 0x46464952 && d32[2] === 0x50424557 && (d32[3] & 0xFFFFFF) === 0x385056) {
switch (d[15]) {
case 0x58: // Extended WebP VP8X --> "X"
return {
mime: 'image/webp',
width: (d[24] | d[25] << 8 | d[26] << 16) + 1,
height: (d[27] | d[28] << 8 | d[29] << 16) + 1
};
case 0x4C: // Lossless WebP VP8L --> "L"
if (d[20] !== 0x2f) return UNSUPPORTED_TYPE;
const dim = d[21] | d[22] << 8 | d[23] << 16 | d[24] << 24;
return {
mime: 'image/webp',
width: (dim & 0x3FFF) + 1,
height: (dim >>> 14 & 0x3FFF) + 1
};
case 0x20: // Lossy WebP VP8 --> " "
if (d[23] !== 0x9d || d[24] !== 0x01 || d[25] !== 0x2a) return UNSUPPORTED_TYPE;
return {
mime: 'image/webp',
width: (d[26] | d[27] << 8) & 0x3FFF,
height: (d[28] | d[29] << 8) & 0x3FFF
};
}
return UNSUPPORTED_TYPE;
}
// AVIF: Box size | ftyp | avif/avis
if (d32[1] === 0x70797466 && (d32[2] === 0x66697661 || d32[2] === 0x73697661)) {
let pos = -1;
// search for ispe box within first 1024 bytes
const limit = Math.min(d.length - 16, 1024);
for (let i = 8; i < limit; i++) {
// scan for ispe
if (d[i] === 0x69 && d[i + 1] === 0x73 && d[i + 2] === 0x70 && d[i + 3] === 0x65) {
pos = i;
break;
}
}
if (pos !== -1) {
// dimensions are in BE at +8 (width) at +12 (height)
const width =
d[pos + 8] << 24 |
d[pos + 9] << 16 |
d[pos + 10] << 8 |
d[pos + 11];
const height =
d[pos + 12] << 24 |
d[pos + 13] << 16 |
d[pos + 14] << 8 |
d[pos + 15];
if (width > 0 && height > 0) {
return { mime: 'image/avif', width, height };
}
}
return UNSUPPORTED_TYPE;
}
return UNSUPPORTED_TYPE;
}
function jpgSize(d: Uint8Array): [number, number] {
const len = d.length;
let i = 4;
let blockLength = d[i] << 8 | d[i + 1];
while (true) {
i += blockLength;
if (i >= len) {
// exhausted without size info
return [0, 0];
}
if (d[i] !== 0xFF) {
return [0, 0];
}
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
if (i + 8 < len) {
return [
d[i + 7] << 8 | d[i + 8],
d[i + 5] << 8 | d[i + 6]
];
}
return [0, 0];
}
i += 2;
blockLength = d[i] << 8 | d[i + 1];
}
}
-68
View File
@@ -1,68 +0,0 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDrawable } from './Types';
export function createCanvas(
localDocument: Document | undefined,
width: number,
height: number,
cssWidth?: number,
cssHeight?: number
): HTMLCanvasElement {
/**
* NOTE: We normally dont care, from which document the canvas
* gets created, so we can fall back to global document,
* if the terminal has no document associated yet.
* This way early image loads before calling .open keep working
* (still discouraged though, as the metrics will be screwed up).
* Only the DOM output canvas should be on the terminal's document,
* which gets explicitly checked in `insertLayerToDom`.
*/
const canvas = (localDocument ?? document).createElement('canvas');
canvas.width = width | 0;
canvas.height = height | 0;
canvas.style.width = `${(cssWidth ?? width) | 0}px`;
canvas.style.height = `${(cssHeight ?? height) | 0}px`;
return canvas;
}
/**
* Drawable is a resource the ImageRenderer knows to draw on the screen.
*/
export class Drawable implements IDrawable {
public readonly width: number;
public readonly height: number;
// @readonly: only for memory tracking
public bytes: number;
constructor(public native: ImageBitmap | VideoFrame | HTMLCanvasElement) {
if (native instanceof VideoFrame) {
this.width = native.displayWidth;
this.height = native.displayHeight;
// NOTE: native.allocationSize() is too expensive
this.bytes = native.codedWidth * native.codedHeight * 4;
} else {
this.width = native.width;
this.height = native.height;
this.bytes = native.width * native.height * 4;
}
}
public close(): void {
if (this.bytes === 0) return;
this.bytes = 0;
try {
if (this.native instanceof HTMLCanvasElement) {
this.native.width = 0;
this.native.height = 0;
} else {
this.native.close?.();
}
} catch {}
this.native = null!;
}
}
+3 -3
View File
@@ -7,9 +7,9 @@ import { SixelImageStorage } from './SixelImageStorage';
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types'; import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors'; import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
import { RGBA8888 } from 'sixel/lib/Types'; import { RGBA8888 } from 'sixel/lib/Types';
import { ImageRenderer } from './ImageRenderer';
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder'; import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
import { createCanvas, Drawable } from './Primitives';
// always free decoder ressources after decoding if it exceeds this limit // always free decoder ressources after decoding if it exceeds this limit
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
@@ -98,12 +98,12 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
return true; return true;
} }
const canvas = createCanvas(undefined, width, height); const canvas = ImageRenderer.createCanvas(undefined, width, height);
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0); canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) { if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
this._dec.release(); this._dec.release();
} }
this._storage.addImage(new Drawable(canvas), undefined, { width, height, mime: 'image/sixel' }); this._storage.addImage(canvas);
return true; return true;
} }
} }
+7 -4
View File
@@ -4,7 +4,7 @@
*/ */
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage'; import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import { IImageAddonOptions, ITerminalExt, IAddImageOpts, IDrawable, IMetrics } from './Types'; import { IImageAddonOptions, ITerminalExt, IAddImageOpts } from './Types';
import { ImageRenderer } from './ImageRenderer'; import { ImageRenderer } from './ImageRenderer';
/** /**
@@ -27,9 +27,9 @@ export class SixelImageStorage {
* Add a sixel image to storage. * Add a sixel image to storage.
* Cursor behavior depends on the sixelScrolling option (DECSET 80). * Cursor behavior depends on the sixelScrolling option (DECSET 80).
*/ */
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics): void { public addImage(img: HTMLCanvasElement | ImageBitmap): void {
this._addImageOpts.scrolling = this._opts.sixelScrolling; this._addImageOpts.scrolling = this._opts.sixelScrolling;
this._storage.addImage(src, data, metrics, this._addImageOpts); this._storage.addImage(img, this._addImageOpts);
} }
/** /**
@@ -39,7 +39,10 @@ export class SixelImageStorage {
*/ */
public advanceCursor(height: number): void { public advanceCursor(height: number): void {
if (this._opts.sixelScrolling) { if (this._opts.sixelScrolling) {
const cellSize = this._renderer.getCellSize() ?? CELL_SIZE_DEFAULT; let cellSize = this._renderer.cellSize;
if (cellSize.width === -1 || cellSize.height === -1) {
cellSize = CELL_SIZE_DEFAULT;
}
const rows = Math.ceil(height / cellSize.height); const rows = Math.ceil(height / cellSize.height);
for (let i = 1; i < rows; ++i) { for (let i = 1; i < rows; ++i) {
this._terminal._core._inputHandler.lineFeed(); this._terminal._core._inputHandler.lineFeed();
+23 -63
View File
@@ -6,10 +6,11 @@
import { IDisposable, IMarker, Terminal } from '@xterm/xterm'; import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
// private imports from base repo we build against // private imports from base repo we build against
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants'; import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/primitives/buffer/Constants';
import type { AttributeData } from 'common/buffer/AttributeData'; import type { AttributeData } from 'common/primitives/buffer/AttributeData';
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types'; import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/primitives/parser/Types';
import type { IInputHandler } from 'common/Types'; import type { IInputHandler } from 'common/Types';
import type { IBufferLine, IExtendedAttrs } from 'common/primitives/buffer/Types';
import type { ITerminal, ReadonlyColorSet } from 'browser/Types'; import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
import type { IRenderDimensions } from 'browser/renderer/shared/Types'; import type { IRenderDimensions } from 'browser/renderer/shared/Types';
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
@@ -47,7 +48,6 @@ export interface IResetHandler {
reset(): void; reset(): void;
} }
/* eslint-disable */
/** /**
* Stub into private interfaces. * Stub into private interfaces.
* This should be kept in line with common libs. * This should be kept in line with common libs.
@@ -55,6 +55,19 @@ export interface IResetHandler {
* have a somewhat reliable testing against code changes in the core repo. * have a somewhat reliable testing against code changes in the core repo.
*/ */
// overloaded IExtendedAttrs to hold image refs
export interface IExtendedAttrsImage extends IExtendedAttrs {
imageId: number;
tileId: number;
clone(): IExtendedAttrsImage;
}
/* eslint-disable */
export interface IBufferLineExt extends IBufferLine {
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
_data: Uint32Array;
}
interface IInputHandlerExt extends IInputHandler { interface IInputHandlerExt extends IInputHandler {
_parser: IEscapeSequenceParser; _parser: IEscapeSequenceParser;
_curAttrData: AttributeData; _curAttrData: AttributeData;
@@ -65,7 +78,6 @@ interface IInputHandlerExt extends IInputHandler {
}; };
onRequestReset(handler: () => void): IDisposable; onRequestReset(handler: () => void): IDisposable;
} }
/* eslint-enable */
export interface ICoreTerminalExt extends ITerminal { export interface ICoreTerminalExt extends ITerminal {
_themeService: IThemeService | undefined; _themeService: IThemeService | undefined;
@@ -77,6 +89,8 @@ export interface ICoreTerminalExt extends ITerminal {
export interface ITerminalExt extends Terminal { export interface ITerminalExt extends Terminal {
_core: ICoreTerminalExt; _core: ICoreTerminalExt;
} }
/* eslint-enable */
/** /**
* Some storage definitions. * Some storage definitions.
@@ -94,70 +108,16 @@ export interface IAddImageOpts {
layer: ImageLayer; layer: ImageLayer;
zIndex: number; zIndex: number;
cursorPos: CursorPos; cursorPos: CursorPos;
prescaleX?: number;
prescaleY?: number;
}
export interface IDrawable {
native: ImageBitmap | VideoFrame | HTMLCanvasElement;
width: number;
height: number;
close(): void;
readonly bytes: number;
}
export type ImageType = 'unsupported'
| 'image/png'
| 'image/jpeg'
| 'image/gif'
| 'image/qoi'
| 'image/webp'
| 'image/avif'
// sixel
| 'image/sixel'
// rgb|a blobs for kitty
| 'image/rgb'
| 'image/rgba';
export interface IMetrics {
mime: ImageType;
width: number;
height: number;
} }
export interface IImageSpec { export interface IImageSpec {
/** entries for a later IImageSource type */ orig: HTMLCanvasElement | ImageBitmap | undefined;
/** drawable for screen rendering */ origCellSize: ICellSize;
src: IDrawable; actual: HTMLCanvasElement | ImageBitmap | undefined;
/** image bytes as blob */ actualCellSize: ICellSize;
data: Blob | undefined;
/** metrics about image like mime and dimensions */
metrics: IMetrics;
/** entries for a later IPlacement type */
/** cell size at time of insert */
cellSize: ICellSize;
/** eviction marker */
marker: IMarker | undefined; marker: IMarker | undefined;
/** used tiles as eviction hint */
tileCount: number; tileCount: number;
/** buffer it was placed on */
bufferType: 'alternate' | 'normal'; bufferType: 'alternate' | 'normal';
/** layer it was placed on */
layer: ImageLayer; layer: ImageLayer;
zIndex: number; zIndex: number;
/** scaling factor on source dimensions, default = 1.0 */
prescaleX: number;
prescaleY: number;
/** crop offset into source, default = 0 (currently unused) */
precropX: number;
precropY: number;
/** output scaling, default = 1.0 (currently unused) */
scaleX: number;
scaleY: number;
/** output offset, default = 0 (currently unused) */
offsetX: number;
offsetY: number;
/** compositing mode on output, default = 'alpha' (currently unused) */
blendMode: 'overwrite' | 'alpha';
} }
@@ -19,7 +19,6 @@ import {
KittyPixelConstants, KittyPixelConstants,
parseKittyCommand parseKittyCommand
} from './KittyGraphicsTypes'; } from './KittyGraphicsTypes';
import { createCanvas } from '../Primitives';
const enum Constants { const enum Constants {
// Memory limit for base64 decoder (4MB, same as IIPHandler) // Memory limit for base64 decoder (4MB, same as IIPHandler)
@@ -574,8 +573,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
bitmap = cropped; bitmap = cropped;
} }
const cw = this._renderer.dimensions?.device.cell.width || CELL_SIZE_DEFAULT.width; const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
const ch = this._renderer.dimensions?.device.cell.height || CELL_SIZE_DEFAULT.height; const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
// Per spec: c/r default to image's natural cell dimensions. // Per spec: c/r default to image's natural cell dimensions.
// If only one of c/r is specified, compute the other from image aspect ratio. // If only one of c/r is specified, compute the other from image aspect ratio.
@@ -637,7 +636,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// the natural cell dimensions. // the natural cell dimensions.
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset; const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset; const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
const offsetCanvas = createCanvas(window.document, canvasW, canvasH); const offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
const offsetCtx = offsetCanvas.getContext('2d'); const offsetCtx = offsetCanvas.getContext('2d');
if (!offsetCtx) { if (!offsetCtx) {
throw new Error('Failed to create offset canvas context'); throw new Error('Failed to create offset canvas context');
@@ -701,7 +700,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
return new Promise<ImageBitmap>((resolve, reject) => { return new Promise<ImageBitmap>((resolve, reject) => {
img.addEventListener('load', () => { img.addEventListener('load', () => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
const canvas = createCanvas(window.document, img.width, img.height); const canvas = ImageRenderer.createCanvas(window.document, img.width, img.height);
canvas.getContext('2d')?.drawImage(img, 0, 0); canvas.getContext('2d')?.drawImage(img, 0, 0);
createImageBitmap(canvas).then(resolve).catch(reject); createImageBitmap(canvas).then(resolve).catch(reject);
}); });
@@ -7,8 +7,6 @@ import { IDisposable } from '@xterm/xterm';
import { ImageStorage } from '../ImageStorage'; import { ImageStorage } from '../ImageStorage';
import { ImageLayer, IAddImageOpts } from '../Types'; import { ImageLayer, IAddImageOpts } from '../Types';
import { IKittyImageData } from './KittyGraphicsTypes'; import { IKittyImageData } from './KittyGraphicsTypes';
import { Drawable } from '../Primitives';
import { UNSUPPORTED_TYPE } from '../Metrics';
// Kitty-specific image storage controller. // Kitty-specific image storage controller.
// //
@@ -104,8 +102,7 @@ export class KittyImageStorage implements IDisposable {
this._addImageOpts.scrolling = scrolling; this._addImageOpts.scrolling = scrolling;
this._addImageOpts.layer = layer; this._addImageOpts.layer = layer;
this._addImageOpts.zIndex = zIndex; this._addImageOpts.zIndex = zIndex;
// FIXME: store image data in blob, fill metrics correctly const storageId = this._storage.addImage(image, this._addImageOpts);
const storageId = this._storage.addImage(new Drawable(image), undefined, UNSUPPORTED_TYPE, this._addImageOpts);
this._kittyIdToStorageId.set(kittyId, storageId); this._kittyIdToStorageId.set(kittyId, storageId);
this._storageIdToKittyId.set(storageId, kittyId); this._storageIdToKittyId.set(storageId, kittyId);
} }
+25 -51
View File
@@ -294,10 +294,10 @@ test.describe('ImageAddon', () => {
test('size params is optional/informative', async () => { test('size params is optional/informative', async () => {
// too small size param // too small size param
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=5:${PALETTE_PNG_BASE64}\x07`); await ctx.proxy.write(`\x1b]1337;File=inline=1;size=5:${PALETTE_PNG_BASE64}\x07`);
deepStrictEqual(await getImageSize(1), [640, 80]); deepStrictEqual(await getOrigSize(1), [640, 80]);
// no size aparm at all // no size aparm at all
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1:${PALETTE_QOI_BASE64}\x07`); await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1:${PALETTE_QOI_BASE64}\x07`);
deepStrictEqual(await getImageSize(2), [640, 80]); deepStrictEqual(await getOrigSize(2), [640, 80]);
}); });
test.skip('FilePart support - bytewise', async () => { test.skip('FilePart support - bytewise', async () => {
// opener // opener
@@ -308,7 +308,7 @@ test.describe('ImageAddon', () => {
} }
// finalizer // finalizer
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`); await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
deepStrictEqual(await getImageSize(1), [640, 80]); deepStrictEqual(await getOrigSize(1), [640, 80]);
}); });
test('FilePart support - chunks', async () => { test('FilePart support - chunks', async () => {
// opener // opener
@@ -319,7 +319,7 @@ test.describe('ImageAddon', () => {
} }
// finalizer // finalizer
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`); await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
deepStrictEqual(await getImageSize(1), [640, 80]); deepStrictEqual(await getOrigSize(1), [640, 80]);
}); });
test('FilePart support - edgecases', async () => { test('FilePart support - edgecases', async () => {
// multiple opener do not harm // multiple opener do not harm
@@ -347,39 +347,39 @@ test.describe('ImageAddon', () => {
// finalizer skipped // finalizer skipped
// write spinfox.png // write spinfox.png
await ctx.proxy.write(TESTDATA_IIP[1][0]); await ctx.proxy.write(TESTDATA_IIP[1][0]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[1][1]); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
}); });
}); });
test.describe('IIP support - testimages', () => { test.describe('IIP support - testimages', () => {
test('palette.png', async () => { test('palette.png', async () => {
await ctx.proxy.write(TESTDATA_IIP[0][0]); await ctx.proxy.write(TESTDATA_IIP[0][0]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[0][1]); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[0][1]);
}); });
test('spinfox.png', async () => { test('spinfox.png', async () => {
await ctx.proxy.write(TESTDATA_IIP[1][0]); await ctx.proxy.write(TESTDATA_IIP[1][0]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[1][1]); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
}); });
test('w3c gif', async () => { test('w3c gif', async () => {
await ctx.proxy.write(TESTDATA_IIP[2][0]); await ctx.proxy.write(TESTDATA_IIP[2][0]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[2][1]); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[2][1]);
}); });
test('w3c jpeg', async () => { test('w3c jpeg', async () => {
await ctx.proxy.write(TESTDATA_IIP[3][0]); await ctx.proxy.write(TESTDATA_IIP[3][0]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[3][1]); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[3][1]);
}); });
test('w3c png', async () => { test('w3c png', async () => {
await ctx.proxy.write(TESTDATA_IIP[4][0]); await ctx.proxy.write(TESTDATA_IIP[4][0]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[4][1]); deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
}); });
}); });
test.describe('IIP - QOI support', () => { test.describe('IIP - QOI support', () => {
test('palette should yield same bytes from PNG and QOI', async () => { test('palette should yield same bytes from PNG and QOI', async () => {
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=525:${PALETTE_PNG_BASE64}\x07`); await ctx.proxy.write(`\x1b]1337;File=inline=1;size=525:${PALETTE_PNG_BASE64}\x07`);
deepStrictEqual(await getImageSize(1), [640, 80]); deepStrictEqual(await getOrigSize(1), [640, 80]);
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1;size=${qoiData.length}:${PALETTE_QOI_BASE64}\x07`); await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1;size=${qoiData.length}:${PALETTE_QOI_BASE64}\x07`);
deepStrictEqual(await getImageSize(2), [640, 80]); deepStrictEqual(await getOrigSize(2), [640, 80]);
const pngScrape = await getImageAtBufferCell(0, 0); const pngScrape = await getImageAtBufferCell(0, 0);
const qoiScrape = await getImageAtBufferCell(0, 11); const qoiScrape = await getImageAtBufferCell(0, 11);
deepStrictEqual(qoiScrape, pngScrape); deepStrictEqual(qoiScrape, pngScrape);
@@ -406,20 +406,20 @@ test.describe('ImageAddon', () => {
const header = 'width=20;height=5;preserveAspectRatio=0'; const header = 'width=20;height=5;preserveAspectRatio=0';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`); await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
const dim = await getDimensions(); const dim = await getDimensions();
deepStrictEqual(await getImageSize(1), [dim.cellWidth * 20, dim.cellHeight * 5]); deepStrictEqual(await getOrigSize(1), [dim.cellWidth * 20, dim.cellHeight * 5]);
}); });
test(name + ': Npx --> width=320px height=160px preserveAspectRatio=0', async () => { test(name + ': Npx --> width=320px height=160px preserveAspectRatio=0', async () => {
// pixel based resize // pixel based resize
const header = 'width=320px;height=160px;preserveAspectRatio=0'; const header = 'width=320px;height=160px;preserveAspectRatio=0';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`); await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
deepStrictEqual(await getImageSize(1), [320, 160]); deepStrictEqual(await getOrigSize(1), [320, 160]);
}); });
test(name + ': N% --> width=50% height=30% preserveAspectRatio=0', async () => { test(name + ': N% --> width=50% height=30% preserveAspectRatio=0', async () => {
// % of viewport resize // % of viewport resize
const header = 'width=50%;height=30%;preserveAspectRatio=0'; const header = 'width=50%;height=30%;preserveAspectRatio=0';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`); await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
const dim = await getDimensions(); const dim = await getDimensions();
deepStrictEqual(await getImageSize(1), [Math.floor(dim.width * 0.5), Math.floor(dim.height * 0.3)]); deepStrictEqual(await getOrigSize(1), [Math.floor(dim.width * 0.5), Math.floor(dim.height * 0.3)]);
}); });
test(name + ': ommitted dimension assumes preserveAspectRatio=1', async () => { test(name + ': ommitted dimension assumes preserveAspectRatio=1', async () => {
// width provided in percent // width provided in percent
@@ -427,23 +427,14 @@ test.describe('ImageAddon', () => {
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`); await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
const dim = await getDimensions(); const dim = await getDimensions();
const width = Math.floor(dim.width * 0.5); const width = Math.floor(dim.width * 0.5);
deepStrictEqual(await getImageSize(1), [width, Math.floor(width * 80 / 640)]); deepStrictEqual(await getOrigSize(1), [width, Math.floor(width * 80 / 640)]);
// height provided in pixel // height provided in pixel
const header2 = 'height=200px'; const header2 = 'height=200px';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header2}:${payload}\x07`); await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header2}:${payload}\x07`);
deepStrictEqual(await getImageSize(2), [Math.floor(200 * 640 / 80), 200]); deepStrictEqual(await getOrigSize(2), [Math.floor(200 * 640 / 80), 200]);
}); });
} }
}); });
test.describe('text overwrite removes tiles', () => {
test('sixel', async () => {
await assertTextClearsTiles(SIXEL_SEQ_0);
});
test('iip', async () => {
await assertTextClearsTiles(TESTDATA_IIP[0][0]);
});
});
}); });
/** /**
@@ -452,10 +443,10 @@ test.describe('ImageAddon', () => {
async function getDimensions(): Promise<IDimensions> { async function getDimensions(): Promise<IDimensions> {
const dimensions: any = await ctx.page.evaluate(`term.dimensions`); const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
return { return {
cellWidth: Math.round(dimensions.device.cell.width), cellWidth: Math.round(dimensions.css.cell.width),
cellHeight: Math.round(dimensions.device.cell.height), cellHeight: Math.round(dimensions.css.cell.height),
width: Math.round(dimensions.device.canvas.width), width: Math.round(dimensions.css.canvas.width),
height: Math.round(dimensions.device.canvas.height) height: Math.round(dimensions.css.canvas.height)
}; };
} }
@@ -471,30 +462,13 @@ async function getScrollbackPlusRows(): Promise<number> {
return ctx.page.evaluate('window.term.options.scrollback + window.term.rows'); return ctx.page.evaluate('window.term.options.scrollback + window.term.rows');
} }
async function getImageSize(id: number): Promise<[number, number]> { async function getOrigSize(id: number): Promise<[number, number]> {
return ctx.page.evaluate<any>(`[ return ctx.page.evaluate<any>(`[
window.imageAddon._storage._images.get(${id}).src.width * window.imageAddon._storage._images.get(${id}).prescaleX, window.imageAddon._storage._images.get(${id}).orig.width,
window.imageAddon._storage._images.get(${id}).src.height * window.imageAddon._storage._images.get(${id}).prescaleY window.imageAddon._storage._images.get(${id}).orig.height
]`).then(r => r.map(Math.floor)); ]`);
} }
async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> { async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> {
return ctx.page.evaluate<any>(`window.imageAddon.getImageAtBufferCell(${x}, ${y})?.toDataURL('image/png')`); return ctx.page.evaluate<any>(`window.imageAddon.getImageAtBufferCell(${x}, ${y})?.toDataURL('image/png')`);
} }
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
return ctx.page.evaluate(`!!window.imageAddon.getImageAtBufferCell(${x}, ${y})`);
}
async function assertTextClearsTiles(imageSeq: string): Promise<void> {
await ctx.proxy.write('\x1b[H' + imageSeq);
await pollFor(ctx.page, '!!window.imageAddon.getImageAtBufferCell(5, 1)', true);
ok(await hasTileAtBufferCell(0, 1));
await ctx.proxy.write('\x1b[2;6H#######');
for (let x = 5; x < 12; x++) {
strictEqual(await hasTileAtBufferCell(x, 1), false);
}
ok(await hasTileAtBufferCell(0, 1));
ok(await hasTileAtBufferCell(13, 1));
ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######'));
}
+26 -45
View File
@@ -123,7 +123,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(seq); await ctx.proxy.write(seq);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [1, 1]); deepStrictEqual(await getOrigSize(1), [1, 1]);
}); });
test('stores 3x1 RGB PNG with a=T', async () => { test('stores 3x1 RGB PNG with a=T', async () => {
@@ -131,7 +131,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(seq); await ctx.proxy.write(seq);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [3, 1]); deepStrictEqual(await getOrigSize(1), [3, 1]);
}); });
test('transmit only (a=t) does not display but stores in handler', async () => { test('transmit only (a=t) does not display but stores in handler', async () => {
@@ -1107,7 +1107,7 @@ test.describe('Kitty Graphics Protocol', () => {
await timeout(200); await timeout(200);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [20, 50]); deepStrictEqual(await getOrigSize(1), [20, 50]);
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]); deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
}); });
@@ -1283,7 +1283,7 @@ test.describe('Kitty Graphics Protocol', () => {
const cursor = await getCursor(); const cursor = await getCursor();
const cellDims: number[] = await ctx.page.evaluate(() => { const cellDims: number[] = await ctx.page.evaluate(() => {
const d = (window as any).term._core._renderService.dimensions.device.cell; const d = (window as any).term._core._renderService.dimensions.css.cell;
return [d.width, d.height]; return [d.width, d.height];
}); });
const expectedR = Math.ceil((100 / 200) * 10 * cellDims[0] / cellDims[1]); const expectedR = Math.ceil((100 / 200) * 10 * cellDims[0] / cellDims[1]);
@@ -1301,7 +1301,7 @@ test.describe('Kitty Graphics Protocol', () => {
const cursor = await getCursor(); const cursor = await getCursor();
const cellDims: number[] = await ctx.page.evaluate(() => { const cellDims: number[] = await ctx.page.evaluate(() => {
const d = (window as any).term._core._renderService.dimensions.device.cell; const d = (window as any).term._core._renderService.dimensions.css.cell;
return [d.width, d.height]; return [d.width, d.height];
}); });
const expectedC = Math.ceil((200 / 100) * 5 * cellDims[1] / cellDims[0]); const expectedC = Math.ceil((200 / 100) * 5 * cellDims[1] / cellDims[0]);
@@ -1535,7 +1535,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200); await timeout(200);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [200, 100]); deepStrictEqual(await getOrigSize(1), [200, 100]);
}); });
test('transmit only (a=t) stores 200x100 image without display', async () => { test('transmit only (a=t) stores 200x100 image without display', async () => {
@@ -1564,7 +1564,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=500;${part2}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=100,i=500;${part2}\x1b\\`);
await timeout(200); await timeout(200);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [200, 100]); deepStrictEqual(await getOrigSize(1), [200, 100]);
}); });
test('handles 3-chunk transmission', async () => { test('handles 3-chunk transmission', async () => {
@@ -1580,7 +1580,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501;${p3}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501;${p3}\x1b\\`);
await timeout(200); await timeout(200);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [200, 100]); deepStrictEqual(await getOrigSize(1), [200, 100]);
}); });
test('verifies chunked data assembles correctly', async () => { test('verifies chunked data assembles correctly', async () => {
@@ -1713,7 +1713,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=20,y=0,w=20,h=50;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=100,x=20,y=0,w=20,h=50;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200); await timeout(200);
deepStrictEqual(await getImageSize(1), [20, 50]); deepStrictEqual(await getOrigSize(1), [20, 50]);
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]); deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
deepStrictEqual(await getPixel(0, 0, 19, 49), [255, 128, 0, 255]); deepStrictEqual(await getPixel(0, 0, 19, 49), [255, 128, 0, 255]);
}); });
@@ -1747,7 +1747,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100,w=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=100,w=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200); await timeout(200);
deepStrictEqual(await getImageSize(1), [200, 100]); deepStrictEqual(await getOrigSize(1), [200, 100]);
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]); deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
deepStrictEqual(await getPixel(0, 0, 199, 99), [255, 255, 255, 255]); deepStrictEqual(await getPixel(0, 0, 199, 99), [255, 255, 255, 255]);
}); });
@@ -1756,7 +1756,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100,h=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=100,h=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200); await timeout(200);
deepStrictEqual(await getImageSize(1), [200, 100]); deepStrictEqual(await getOrigSize(1), [200, 100]);
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]); deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
deepStrictEqual(await getPixel(0, 0, 0, 50), [255, 192, 203, 255]); deepStrictEqual(await getPixel(0, 0, 0, 50), [255, 192, 203, 255]);
}); });
@@ -1772,7 +1772,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=-10,y=-10;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=100,x=-10,y=-10;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200); await timeout(200);
deepStrictEqual(await getImageSize(1), [200, 100]); deepStrictEqual(await getOrigSize(1), [200, 100]);
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]); deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
}); });
@@ -1893,28 +1893,28 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=3,v=1;${RAW_RGB_3X1}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=24,s=3,v=1;${RAW_RGB_3X1}\x1b\\`);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [3, 1]); deepStrictEqual(await getOrigSize(1), [3, 1]);
}); });
test('stores image with correct original dimensions (2x2)', async () => { test('stores image with correct original dimensions (2x2)', async () => {
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=2,v=2;${RAW_RGB_2X2}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=24,s=2,v=2;${RAW_RGB_2X2}\x1b\\`);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [2, 2]); deepStrictEqual(await getOrigSize(1), [2, 2]);
}); });
test('stores image with correct original dimensions (5x1)', async () => { test('stores image with correct original dimensions (5x1)', async () => {
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=5,v=1;${RAW_RGB_5X1}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=24,s=5,v=1;${RAW_RGB_5X1}\x1b\\`);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [5, 1]); deepStrictEqual(await getOrigSize(1), [5, 1]);
}); });
test('stores image with correct original dimensions (4x2)', async () => { test('stores image with correct original dimensions (4x2)', async () => {
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=4,v=2;${RAW_RGB_4X2}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=24,s=4,v=2;${RAW_RGB_4X2}\x1b\\`);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [4, 2]); deepStrictEqual(await getOrigSize(1), [4, 2]);
}); });
}); });
@@ -2034,21 +2034,21 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=3,v=1;${RAW_RGBA_3X1}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=32,s=3,v=1;${RAW_RGBA_3X1}\x1b\\`);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [3, 1]); deepStrictEqual(await getOrigSize(1), [3, 1]);
}); });
test('stores image with correct original dimensions (2x2)', async () => { test('stores image with correct original dimensions (2x2)', async () => {
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=2,v=2;${RAW_RGBA_2X2}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=32,s=2,v=2;${RAW_RGBA_2X2}\x1b\\`);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [2, 2]); deepStrictEqual(await getOrigSize(1), [2, 2]);
}); });
test('stores image with correct original dimensions (5x1)', async () => { test('stores image with correct original dimensions (5x1)', async () => {
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=5,v=1;${RAW_RGBA_5X1}\x1b\\`); await ctx.proxy.write(`\x1b_Ga=T,f=32,s=5,v=1;${RAW_RGBA_5X1}\x1b\\`);
await timeout(100); await timeout(100);
strictEqual(await getImageStorageLength(), 1); strictEqual(await getImageStorageLength(), 1);
deepStrictEqual(await getImageSize(1), [5, 1]); deepStrictEqual(await getOrigSize(1), [5, 1]);
}); });
}); });
@@ -2213,21 +2213,6 @@ test.describe('Kitty Graphics Protocol', () => {
await pollFor(ctx.page, 'window._imageAddedCount', 2); await pollFor(ctx.page, 'window._imageAddedCount', 2);
}); });
}); });
test.describe('text overwrite removes tiles', () => {
test('a=T placement', async () => {
await ctx.proxy.write(`\x1b[H\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await pollFor(ctx.page, '!!window.imageAddon.getImageAtBufferCell(5, 1)', true);
ok(await hasTileAtBufferCell(0, 1));
await ctx.proxy.write('\x1b[2;6H#######');
for (let x = 5; x < 12; x++) {
strictEqual(await hasTileAtBufferCell(x, 1), false);
}
ok(await hasTileAtBufferCell(0, 1));
ok(await hasTileAtBufferCell(13, 1));
ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######'));
});
});
}); });
/** /**
@@ -2236,10 +2221,10 @@ test.describe('Kitty Graphics Protocol', () => {
async function getDimensions(): Promise<IDimensions> { async function getDimensions(): Promise<IDimensions> {
const dimensions: any = await ctx.page.evaluate(`term.dimensions`); const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
return { return {
cellWidth: Math.round(dimensions.device.cell.width), cellWidth: Math.round(dimensions.css.cell.width),
cellHeight: Math.round(dimensions.device.cell.height), cellHeight: Math.round(dimensions.css.cell.height),
width: Math.round(dimensions.device.canvas.width), width: Math.round(dimensions.css.canvas.width),
height: Math.round(dimensions.device.canvas.height) height: Math.round(dimensions.css.canvas.height)
}; };
} }
@@ -2251,10 +2236,10 @@ async function getImageStorageLength(): Promise<number> {
return ctx.page.evaluate('window.imageAddon._storage._images.size'); return ctx.page.evaluate('window.imageAddon._storage._images.size');
} }
async function getImageSize(id: number): Promise<[number, number]> { async function getOrigSize(id: number): Promise<[number, number]> {
return ctx.page.evaluate<any>(`[ return ctx.page.evaluate<any>(`[
window.imageAddon._storage._images.get(${id}).src.width, window.imageAddon._storage._images.get(${id}).orig.width,
window.imageAddon._storage._images.get(${id}).src.height window.imageAddon._storage._images.get(${id}).orig.height
]`); ]`);
} }
@@ -2277,7 +2262,3 @@ async function getPixels(col: number, row: number, x: number, y: number, w: numb
return Array.from(ctx2d.getImageData(x, y, w, h).data); return Array.from(ctx2d.getImageData(x, y, w, h).data);
}, [col, row, x, y, w, h]); }, [col, row, x, y, w, h]);
} }
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
return ctx.page.evaluate(`!!window.imageAddon.getImageAtBufferCell(${x}, ${y})`);
}
+5
View File
@@ -125,5 +125,10 @@ declare module '@xterm/addon-image' {
* Get original image canvas at buffer position. * Get original image canvas at buffer position.
*/ */
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined; public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
/**
* Extract single tile canvas at buffer position.
*/
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress'; import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress';
import type { Emitter, IEvent } from 'common/Event'; import type { Emitter, IEvent } from 'common/primitives/Event';
const enum ProgressType { const enum ProgressType {
@@ -10,7 +10,7 @@ import { SearchLineCache } from './SearchLineCache';
import { Terminal } from 'browser/public/Terminal'; import { Terminal } from 'browser/public/Terminal';
import type { ISearchDecorationOptions } from '@xterm/addon-search'; import type { ISearchDecorationOptions } from '@xterm/addon-search';
import type { IDecorationOptions } from '@xterm/xterm'; import type { IDecorationOptions } from '@xterm/xterm';
import { DisposableStore } from 'common/Lifecycle'; import { DisposableStore } from 'common/primitives/Lifecycle';
function writeP(terminal: Terminal, data: string): Promise<void> { function writeP(terminal: Terminal, data: string): Promise<void> {
return new Promise(r => terminal.write(data, r)); return new Promise(r => terminal.write(data, r));
+1 -1
View File
@@ -5,7 +5,7 @@
import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm'; import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';
import type { ISearchDecorationOptions } from '@xterm/addon-search'; import type { ISearchDecorationOptions } from '@xterm/addon-search';
import { dispose, Disposable, toDisposable } from 'common/Lifecycle'; import { dispose, Disposable, toDisposable } from 'common/primitives/Lifecycle';
import type { ISearchResult } from './SearchEngine'; import type { ISearchResult } from './SearchEngine';
/** /**
+3 -3
View File
@@ -5,9 +5,9 @@
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm'; import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search'; import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';
import { Emitter, type IEvent } from 'common/Event'; import { Emitter, type IEvent } from 'common/primitives/Event';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { Disposable, MutableDisposable, toDisposable } from 'common/primitives/Lifecycle';
import { disposableTimeout } from 'common/Async'; import { disposableTimeout } from 'common/primitives/Async';
import { SearchLineCache } from './SearchLineCache'; import { SearchLineCache } from './SearchLineCache';
import { SearchState } from './SearchState'; import { SearchState } from './SearchState';
import { SearchEngine, type ISearchResult } from './SearchEngine'; import { SearchEngine, type ISearchResult } from './SearchEngine';
+1 -1
View File
@@ -7,7 +7,7 @@ import { SearchEngine } from './SearchEngine';
import { SearchLineCache } from './SearchLineCache'; import { SearchLineCache } from './SearchLineCache';
import { Terminal } from 'browser/public/Terminal'; import { Terminal } from 'browser/public/Terminal';
import type { ISearchOptions } from '@xterm/addon-search'; import type { ISearchOptions } from '@xterm/addon-search';
import { DisposableStore } from 'common/Lifecycle'; import { DisposableStore } from 'common/primitives/Lifecycle';
function writeP(terminal: Terminal, data: string): Promise<void> { function writeP(terminal: Terminal, data: string): Promise<void> {
return new Promise(r => terminal.write(data, r)); return new Promise(r => terminal.write(data, r));
@@ -5,7 +5,7 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { SearchLineCache, LineCacheEntry } from './SearchLineCache'; import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
import { Terminal } from 'browser/public/Terminal'; import { Terminal } from 'browser/public/Terminal';
import { timeout } from 'common/Async'; import { timeout } from 'common/primitives/Async';
function writeP(terminal: Terminal, data: string): Promise<void> { function writeP(terminal: Terminal, data: string): Promise<void> {
return new Promise(r => terminal.write(data, r)); return new Promise(r => terminal.write(data, r));
+2 -2
View File
@@ -4,8 +4,8 @@
*/ */
import type { Terminal } from '@xterm/xterm'; import type { Terminal } from '@xterm/xterm';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/primitives/Lifecycle';
import { disposableTimeout } from 'common/Async'; import { disposableTimeout } from 'common/primitives/Async';
export type LineCacheEntry = [ export type LineCacheEntry = [
/** /**
@@ -5,8 +5,8 @@
import type { ISearchResultChangeEvent } from '@xterm/addon-search'; import type { ISearchResultChangeEvent } from '@xterm/addon-search';
import type { IDisposable } from '@xterm/xterm'; import type { IDisposable } from '@xterm/xterm';
import { Emitter, type IEvent } from 'common/Event'; import { Emitter, type IEvent } from 'common/primitives/Event';
import { Disposable } from 'common/Lifecycle'; import { Disposable } from 'common/primitives/Lifecycle';
import type { ISearchResult } from './SearchEngine'; import type { ISearchResult } from './SearchEngine';
/** /**
@@ -6,7 +6,7 @@
import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark'; import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
import { spawn } from 'node-pty'; import { spawn } from 'node-pty';
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; import { Utf8ToUtf32, stringFromCodePoint } from 'common/primitives/input/TextDecoder';
import { Terminal } from 'browser/public/Terminal'; import { Terminal } from 'browser/public/Terminal';
import { SerializeAddon } from 'SerializeAddon'; import { SerializeAddon } from 'SerializeAddon';
+3 -3
View File
@@ -7,10 +7,10 @@
import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm'; import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize'; import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
import { IColor } from 'common/Types'; import { IColor } from 'common/primitives/Types';
import { IAttributeData } from 'common/buffer/Types'; import { IAttributeData } from 'common/primitives/buffer/Types';
import { DEFAULT_ANSI_COLORS } from 'browser/Types'; import { DEFAULT_ANSI_COLORS } from 'browser/Types';
import { UnderlineStyle } from 'common/buffer/Constants'; import { UnderlineStyle } from 'common/primitives/buffer/Constants';
function constrain(value: number, low: number, high: number): number { function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high)); return Math.max(low, Math.min(value, high));
@@ -6,7 +6,7 @@
import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark'; import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
import { spawn } from 'node-pty'; import { spawn } from 'node-pty';
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; import { Utf8ToUtf32, stringFromCodePoint } from 'common/primitives/input/TextDecoder';
import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal'; import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal';
import { UnicodeGraphemeProvider } from 'UnicodeGraphemeProvider'; import { UnicodeGraphemeProvider } from 'UnicodeGraphemeProvider';
+3 -3
View File
@@ -1,11 +1,11 @@
import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ReadonlyColorSet } from 'browser/Types'; import { ReadonlyColorSet } from 'browser/Types';
import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants'; import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/primitives/buffer/Constants';
import { IDecorationService, IOptionsService } from 'common/services/Services'; import { IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData } from 'common/buffer/Types'; import { ICellData } from 'common/primitives/buffer/Types';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { rgba } from 'common/Color'; import { rgba } from 'common/primitives/Color';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils'; import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions'; import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions';
@@ -6,8 +6,8 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { configEquals } from './CharAtlasUtils'; import { configEquals } from './CharAtlasUtils';
import { ICharAtlasConfig } from './Types'; import { ICharAtlasConfig } from './Types';
import { NULL_COLOR } from 'common/Color'; import { NULL_COLOR } from 'common/primitives/Color';
import { IColor } from 'common/Types'; import { IColor } from 'common/primitives/Types';
function createTestConfig(overrides: Partial<ICharAtlasConfig> = {}): ICharAtlasConfig { function createTestConfig(overrides: Partial<ICharAtlasConfig> = {}): ICharAtlasConfig {
const color: IColor = { css: '#ffffff', rgba: 0xffffffff }; const color: IColor = { css: '#ffffff', rgba: 0xffffffff };
+2 -2
View File
@@ -4,10 +4,10 @@
*/ */
import { ICharAtlasConfig } from './Types'; import { ICharAtlasConfig } from './Types';
import { Attributes } from 'common/buffer/Constants'; import { Attributes } from 'common/primitives/buffer/Constants';
import { ITerminalOptions } from '@xterm/xterm'; import { ITerminalOptions } from '@xterm/xterm';
import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { NULL_COLOR } from 'common/Color'; import { NULL_COLOR } from 'common/primitives/Color';
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number, customGlyphs: boolean = true): ICharAtlasConfig { export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number, customGlyphs: boolean = true): ICharAtlasConfig {
// null out some fields that don't matter // null out some fields that don't matter
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { isFirefox, isLegacyEdge } from 'common/Platform'; import { isFirefox, isLegacyEdge } from 'common/primitives/Platform';
export const DIM_OPACITY = 0.5; export const DIM_OPACITY = 0.5;
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge // The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { toDisposable, IDisposable } from 'common/Lifecycle'; import { toDisposable, IDisposable } from 'common/primitives/Lifecycle';
export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable {
// Observe any resizes to the element and extract the actual pixel size of the element if the // Observe any resizes to the element and extract the actual pixel size of the element if the
+4 -24
View File
@@ -4,8 +4,8 @@
*/ */
import { TextureAtlas } from './TextureAtlas'; import { TextureAtlas } from './TextureAtlas';
import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { NULL_CELL_CODE } from 'common/primitives/buffer/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle'; import { Disposable, toDisposable } from 'common/primitives/Lifecycle';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types'; import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types';
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils'; import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
@@ -100,8 +100,6 @@ export class GlyphRenderer extends Disposable {
private readonly _attributesBuffer: WebGLBuffer; private readonly _attributesBuffer: WebGLBuffer;
private _atlas: ITextureAtlas | undefined; private _atlas: ITextureAtlas | undefined;
private _lastSeenPageLayoutVersion: number = -1;
private _pageOverflowWarned: boolean = false;
private _activeBuffer: number = 0; private _activeBuffer: number = 0;
private readonly _vertices: IVertices = { private readonly _vertices: IVertices = {
count: 0, count: 0,
@@ -215,19 +213,8 @@ export class GlyphRenderer extends Disposable {
this.handleResize(); this.handleResize();
} }
/**
* Call when a frame is being drawn. Returns whether the full model must be rebuilt before
* rendering this frame because the atlas page layout changed since this renderer last drew.
*/
public beginFrame(): boolean { public beginFrame(): boolean {
if (!this._atlas) { return this._atlas ? this._atlas.beginFrame() : true;
return true;
}
if (this._atlas.pageLayoutVersion !== this._lastSeenPageLayoutVersion) {
this._lastSeenPageLayoutVersion = this._atlas.pageLayoutVersion;
return true;
}
return false;
} }
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void { public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
@@ -376,13 +363,7 @@ export class GlyphRenderer extends Disposable {
// Bind the atlas page texture if they have changed. AtlasPage.version is globally // Bind the atlas page texture if they have changed. AtlasPage.version is globally
// monotonic, so a page object swap at the same index (which happens after a page merge) // monotonic, so a page object swap at the same index (which happens after a page merge)
// is detected by the same comparison. // is detected by the same comparison.
// Clamp defensively in case the atlas unexpectedly exceeds texture capacity. for (let i = 0; i < this._atlas.pages.length; i++) {
const pageCount = Math.min(this._atlas.pages.length, this._atlasTextures.length);
if (this._atlas.pages.length > this._atlasTextures.length && !this._pageOverflowWarned) {
this._pageOverflowWarned = true;
this._logService.warn(`Atlas page count (${this._atlas.pages.length}) exceeds the renderer's texture capacity (${this._atlasTextures.length}), some glyphs will not render correctly`);
}
for (let i = 0; i < pageCount; i++) {
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) { if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
this._bindAtlasPageTexture(gl, this._atlas, i); this._bindAtlasPageTexture(gl, this._atlas, i);
} }
@@ -394,7 +375,6 @@ export class GlyphRenderer extends Disposable {
public setAtlas(atlas: ITextureAtlas): void { public setAtlas(atlas: ITextureAtlas): void {
this._atlas = atlas; this._atlas = atlas;
this._lastSeenPageLayoutVersion = -1;
this.invalidateAtlasTextures(); this.invalidateAtlasTextures();
} }
+3 -3
View File
@@ -6,9 +6,9 @@
import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { IThemeService } from 'browser/services/Services'; import { IThemeService } from 'browser/services/Services';
import { ReadonlyColorSet } from 'browser/Types'; import { ReadonlyColorSet } from 'browser/Types';
import { Attributes, FgFlags } from 'common/buffer/Constants'; import { Attributes, FgFlags } from 'common/primitives/buffer/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle'; import { Disposable, toDisposable } from 'common/primitives/Lifecycle';
import { IColor } from 'common/Types'; import { IColor } from 'common/primitives/Types';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { RenderModelConstants } from './RenderModel'; import { RenderModelConstants } from './RenderModel';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types'; import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
+17 -41
View File
@@ -8,14 +8,14 @@ import { DIM_OPACITY, TEXT_BASELINE } from './Constants';
import { tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer'; import { tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer';
import { computeNextVariantOffset, treatGlyphAsBackgroundColor, 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 './Types'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from './Types';
import { NULL_COLOR, channels, color, rgba } from 'common/Color'; import { NULL_COLOR, channels, color, rgba } from 'common/primitives/Color';
import { FourKeyMap } from 'common/MultiKeyMap'; import { FourKeyMap } from 'common/primitives/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue'; import { IdleTaskQueue } from 'common/primitives/TaskQueue';
import { IColor } from 'common/Types'; import { IColor } from 'common/primitives/Types';
import { AttributeData } from 'common/buffer/AttributeData'; import { AttributeData } from 'common/primitives/buffer/AttributeData';
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants'; import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/primitives/buffer/Constants';
import { ILogService, IUnicodeService } from 'common/services/Services'; import { ILogService, IUnicodeService } from 'common/services/Services';
import { Emitter } from 'common/Event'; import { Emitter } from 'common/primitives/Event';
/** /**
* A shared object which is used to draw nothing for a particular cell. * A shared object which is used to draw nothing for a particular cell.
@@ -132,8 +132,12 @@ export class TextureAtlas implements ITextureAtlas {
} }
} }
private _pageLayoutVersion = 0; private _requestClearModel = false;
public get pageLayoutVersion(): number { return this._pageLayoutVersion; } public beginFrame(): boolean {
const result = this._requestClearModel;
this._requestClearModel = false;
return result;
}
public clearTexture(): void { public clearTexture(): void {
if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) { if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) {
@@ -145,11 +149,6 @@ export class TextureAtlas implements ITextureAtlas {
this._cacheMap.clear(); this._cacheMap.clear();
this._cacheMapCombined.clear(); this._cacheMapCombined.clear();
this._didWarmUp = false; this._didWarmUp = false;
// Invalidate renderer models so all texture pages are refreshed. The atlas may be shared, in
// which case the clearing renderer has cleared only its own model and every other owner still
// holds texture coords into the rows just wiped.
this._pageLayoutVersion++;
} }
private _createNewPage(): AtlasPage { private _createNewPage(): AtlasPage {
@@ -185,8 +184,6 @@ export class TextureAtlas implements ITextureAtlas {
// Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot // Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
// effectively reduce page count and merging would cause issues. // effectively reduce page count and merging would cause issues.
if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) { if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
// Evict instead of adding a page beyond the renderer's texture capacity.
this._evictAllPages();
const newPage = new AtlasPage(this._document, this._textureSize); const newPage = new AtlasPage(this._document, this._textureSize);
this._pages.push(newPage); this._pages.push(newPage);
this._activePages.push(newPage); this._activePages.push(newPage);
@@ -209,8 +206,8 @@ export class TextureAtlas implements ITextureAtlas {
// Add the new merged page to the end // Add the new merged page to the end
this.pages.push(mergedPage); this.pages.push(mergedPage);
// Invalidate renderer models so all texture pages are refreshed. // Request the model to be cleared to refresh all texture pages.
this._pageLayoutVersion++; this._requestClearModel = true;
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas); this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
} }
@@ -261,23 +258,6 @@ export class TextureAtlas implements ITextureAtlas {
} }
} }
private _evictAllPages(): void {
const startTime = performance.now();
const pageCount = this._pages.length;
for (const page of this._pages) {
this._onRemoveTextureAtlasCanvas.fire(page.canvas);
page.canvas.remove();
}
this._pages.length = 0;
this._activePages.length = 0;
this._overflowSizePage = undefined;
this._cacheMap.clear();
this._cacheMapCombined.clear();
this._didWarmUp = false;
this._pageLayoutVersion++;
this._logService.debug(`Evicted ${pageCount} WebGL atlas pages in ${(performance.now() - startTime).toFixed(2)}ms`);
}
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph { public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer); return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer);
} }
@@ -835,15 +815,11 @@ export class TextureAtlas implements ITextureAtlas {
// Create a new page for oversized glyphs as they come up // Create a new page for oversized glyphs as they come up
if (rasterizedGlyph.size.x > this._textureSize) { if (rasterizedGlyph.size.x > this._textureSize) {
if (!this._overflowSizePage) { if (!this._overflowSizePage) {
// Make room for the oversized page without exceeding texture capacity.
if (TextureAtlas.maxAtlasPages && this._pages.length >= TextureAtlas.maxAtlasPages) {
this._evictAllPages();
}
this._overflowSizePage = new AtlasPage(this._document, this._config.deviceMaxTextureSize); this._overflowSizePage = new AtlasPage(this._document, this._config.deviceMaxTextureSize);
this.pages.push(this._overflowSizePage); this.pages.push(this._overflowSizePage);
// Invalidate renderer models so all texture pages are refreshed. // Request the model to be cleared to refresh all texture pages.
this._pageLayoutVersion++; this._requestClearModel = true;
this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas); this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas);
} }
activePage = this._overflowSizePage; activePage = this._overflowSizePage;
+5 -7
View File
@@ -6,8 +6,8 @@
import { FontWeight } from '@xterm/xterm'; import { FontWeight } from '@xterm/xterm';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types'; import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/primitives/Types';
import type { IEvent } from 'common/Event'; import type { IEvent } from 'common/primitives/Event';
export interface IRenderModel { export interface IRenderModel {
cells: Uint32Array; cells: Uint32Array;
@@ -68,12 +68,10 @@ export interface ITextureAtlas extends IDisposable {
warmUp(): void; warmUp(): void;
/** /**
* Incremented whenever cached glyph texture page mappings may be stale, such as after atlas page * Call when a frame is being drawn, this will return true if the atlas was cleared to make room
* merges or overflow page creation. Renderers compare this against their own last-seen value and * for a new set of glyphs.
* rebuild their model when it changes; a shared atlas can have many renderers, so this must not
* be a consume-once flag.
*/ */
readonly pageLayoutVersion: number; beginFrame(): boolean;
/** /**
* Clear all glyphs from the texture atlas. * Clear all glyphs from the texture atlas.
+3 -3
View File
@@ -7,12 +7,12 @@ import type { ITerminalAddon, Terminal } from '@xterm/xterm';
import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl'; import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ITerminal } from 'browser/Types'; import { ITerminal } from 'browser/Types';
import { Disposable, toDisposable } from 'common/Lifecycle'; import { Disposable, toDisposable } from 'common/primitives/Lifecycle';
import { getSafariVersion, isSafari } from 'common/Platform'; import { getSafariVersion, isSafari } from 'common/primitives/Platform';
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { IWebGL2RenderingContext } from './Types'; import { IWebGL2RenderingContext } from './Types';
import { WebglRenderer } from './WebglRenderer'; import { WebglRenderer } from './WebglRenderer';
import { Emitter, EventUtils } from 'common/Event'; import { Emitter, EventUtils } from 'common/primitives/Event';
export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi { export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi {
private _terminal?: Terminal; private _terminal?: Terminal;
+6 -6
View File
@@ -10,10 +10,10 @@ import { CursorBlinkStateManager } from './CursorBlinkStateManager';
import { observeDevicePixelDimensions } from './DevicePixelObserver'; import { observeDevicePixelDimensions } from './DevicePixelObserver';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { CharData, IBufferLine, ICellData } from 'common/buffer/Types'; import { CharData, IBufferLine, ICellData } from 'common/primitives/buffer/Types';
import { AttributeData } from 'common/buffer/AttributeData'; import { AttributeData } from 'common/primitives/buffer/AttributeData';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/primitives/buffer/CellData';
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/primitives/buffer/Constants';
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager'; import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
@@ -23,9 +23,9 @@ import { COMBINED_CHAR_BIT_MASK, RenderModel, RenderModelConstants } from './Ren
import { IWebGL2RenderingContext, type ITextureAtlas } from './Types'; import { IWebGL2RenderingContext, type ITextureAtlas } from './Types';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { IRenderLayer } from './renderLayer/Types'; import { IRenderLayer } from './renderLayer/Types';
import { Emitter, EventUtils } from 'common/Event'; import { Emitter, EventUtils } from 'common/primitives/Event';
import { addDisposableListener } from 'browser/Dom'; import { addDisposableListener } from 'browser/Dom';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/primitives/Lifecycle';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
const enum Constants { const enum Constants {
@@ -1,54 +0,0 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { createPatternCanvas } from './CustomGlyphRasterizer';
describe('CustomGlyphRasterizer', () => {
describe('createPatternCanvas', () => {
it('prefers an offscreen canvas without using the DOM canvas factory', () => {
const expectedCanvas = {} as OffscreenCanvas;
const calls: string[] = [];
const canvas = createPatternCanvas(
2,
3,
(width, height) => {
calls.push(`offscreen:${width}x${height}`);
return expectedCanvas;
},
() => {
calls.push('dom');
return {} as HTMLCanvasElement;
}
);
assert.deepEqual({ canvas, calls }, {
canvas: expectedCanvas,
calls: ['offscreen:2x3']
});
});
it('uses the main-realm DOM canvas factory when OffscreenCanvas is unavailable', () => {
const expectedCanvas = {} as HTMLCanvasElement;
const calls: string[] = [];
const canvas = createPatternCanvas(
4,
5,
undefined,
(width, height) => {
calls.push(`dom:${width}x${height}`);
return expectedCanvas;
}
);
assert.deepEqual({ canvas, calls }, {
canvas: expectedCanvas,
calls: ['dom:4x5']
});
});
});
});
@@ -8,29 +8,6 @@ import type { ILogService } from 'common/services/Services';
import { customGlyphDefinitions } from './CustomGlyphDefinitions'; import { customGlyphDefinitions } from './CustomGlyphDefinitions';
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types'; import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
type PatternCanvas = HTMLCanvasElement | OffscreenCanvas;
type PatternCanvasFactory = (width: number, height: number) => PatternCanvas;
const createOffscreenPatternCanvas: PatternCanvasFactory | undefined = typeof OffscreenCanvas === 'undefined'
? undefined
: (width, height) => new OffscreenCanvas(width, height);
const createDomPatternCanvas: PatternCanvasFactory = (width, height) => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
export function createPatternCanvas(
width: number,
height: number,
offscreenCanvasFactory: PatternCanvasFactory | undefined = createOffscreenPatternCanvas,
domCanvasFactory: PatternCanvasFactory = createDomPatternCanvas
): PatternCanvas {
return offscreenCanvasFactory?.(width, height) ?? domCanvasFactory(width, height);
}
/** /**
* Try drawing a custom block element or box drawing character, returning whether it was * Try drawing a custom block element or box drawing character, returning whether it was
* successfully drawn. * successfully drawn.
@@ -481,9 +458,9 @@ function drawPatternChar(
if (!pattern) { if (!pattern) {
const width = charDefinition[0].length; const width = charDefinition[0].length;
const height = charDefinition.length; const height = charDefinition.length;
// The atlas canvas can be adopted into another document, so temporary resources must not use const tmpCanvas = ctx.canvas.ownerDocument.createElement('canvas');
// its mutable ownerDocument. tmpCanvas.width = width;
const tmpCanvas = createPatternCanvas(width, height); tmpCanvas.height = height;
const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d')); const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
const imageData = new ImageData(width, height); const imageData = new ImageData(width, height);
@@ -7,8 +7,8 @@ import { ReadonlyColorSet } from 'browser/Types';
import { acquireTextureAtlas } from '../CharAtlasCache'; import { acquireTextureAtlas } from '../CharAtlasCache';
import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle'; import { Disposable, toDisposable } from 'common/primitives/Lifecycle';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/primitives/buffer/CellData';
import { IOptionsService } from 'common/services/Services'; import { IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { IRenderLayer } from './Types'; import { IRenderLayer } from './Types';
@@ -1,481 +0,0 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IImage32, decodePng } from '@lunapaint/png-codec';
import test, { expect } from '@playwright/test';
import type { Terminal, ITerminalInitOnlyOptions, ITerminalOptions } from '@xterm/xterm';
import type { IWebglAddonOptions, WebglAddon } from '@xterm/addon-webgl';
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
type CellSignature = number[];
type TestTerminalConstructor = new (options?: ITerminalOptions & ITerminalInitOnlyOptions) => ITestTerminal;
type TestWebglAddonConstructor = new (options?: IWebglAddonOptions) => ITestWebglAddon;
interface ITestTextureAtlasConstructor {
maxAtlasPages: number | undefined;
maxTextureSize: number | undefined;
}
interface ITestAtlasPage {
canvas: HTMLCanvasElement;
}
interface ITestTextureAtlas {
constructor: ITestTextureAtlasConstructor;
pages: ITestAtlasPage[];
pageLayoutVersion: number;
_overflowSizePage?: ITestAtlasPage;
_textureSize: number;
}
interface ITestGlyphRenderer {
_atlasTextures: unknown[];
}
interface ITestRenderer {
_charAtlas?: ITestTextureAtlas;
_glyphRenderer?: {
value?: ITestGlyphRenderer;
};
dimensions: {
device: {
cell: {
width: number;
};
};
};
}
interface ITestRenderService {
_renderer?: {
value?: ITestRenderer;
};
}
interface ITestTerminal extends Terminal {
_core?: {
_renderService?: ITestRenderService;
};
}
interface ITestWebglAddon extends WebglAddon {
_renderer?: ITestRenderer;
}
interface IAtlasEventStats {
removals: number;
maxPages: number;
}
interface ITestWindow extends Window {
Terminal: TestTerminalConstructor;
WebglAddon: TestWebglAddonConstructor;
term: ITestTerminal;
termB?: ITestTerminal;
addon?: ITestWebglAddon;
addonB?: ITestWebglAddon;
atlasEventStats?: IAtlasEventStats;
}
interface IAtlasLimits {
maxAtlasPages: number;
maxTextureSize: number;
}
interface IAtlasState {
pages: number;
atlasTextures: number;
pageLayoutVersion: number;
overflowPageCreated: boolean;
removals: number;
maxPages: number;
}
const NORMAL_HEADER = ' NORMAL_REF_0123456789';
const POST_EVICTION_TEXT = ' AFTER_EVICTION_0123456789';
const NORMAL_HEADER_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const POST_EVICTION_COLS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const TERM_B_SELECTOR = '#terminal-container-b .xterm-screen';
const TERM_SELECTOR = '#terminal-container .xterm-screen';
async function loadWebglStrict(ctx: ITestContext): Promise<void> {
await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
w.addon = new w.WebglAddon({ preserveDrawingBuffer: true });
w.term.loadAddon(w.addon);
});
const isWebglRenderer = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
return !!w.addon && w.term._core?._renderService?._renderer?.value === w.addon._renderer;
});
expect(isWebglRenderer, 'WebGL renderer must be active').toBe(true);
}
async function createTerminalB(ctx: ITestContext, cols: number): Promise<void> {
await ctx.page.evaluate(columnCount => {
const w = window as unknown as ITestWindow;
w.termB?.dispose();
document.getElementById('terminal-container-b')?.remove();
const element = document.createElement('div');
element.id = 'terminal-container-b';
document.body.appendChild(element);
w.termB = new w.Terminal({ cols: columnCount, rows: 24, allowProposedApi: true });
w.termB.open(element);
w.addonB = new w.WebglAddon({ preserveDrawingBuffer: true });
w.termB.loadAddon(w.addonB);
}, cols);
}
async function writeToBAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
await ctx.page.evaluate(value => new Promise<void>(resolve => {
const w = window as unknown as ITestWindow;
if (!w.termB) {
throw new Error('Terminal B must be created before writing to it');
}
const disposable = w.termB.onRender(() => {
disposable.dispose();
resolve();
});
w.termB.write(value);
}), data);
}
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
const renderPromise = new Promise<void>(resolve => {
const disposable = ctx.proxy.onRender(() => {
disposable.dispose();
resolve();
});
});
await ctx.proxy.write(data);
await renderPromise;
}
async function refreshAndWaitForRender(ctx: ITestContext, terminal: 'term' | 'termB'): Promise<void> {
await ctx.page.evaluate(name => new Promise<void>(resolve => {
const w = window as unknown as ITestWindow;
const term = w[name];
if (!term) {
throw new Error(`${name} must exist before refreshing it`);
}
const disposable = term.onRender(() => {
disposable.dispose();
resolve();
});
term.refresh(0, term.rows - 1);
}), terminal);
}
async function configureAtlasLimits(ctx: ITestContext): Promise<IAtlasLimits> {
const limits = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
const atlas = w.term._core?._renderService?._renderer?.value?._charAtlas;
if (!atlas || atlas.constructor.maxAtlasPages === undefined || atlas.constructor.maxTextureSize === undefined) {
return undefined;
}
const result = {
maxAtlasPages: atlas.constructor.maxAtlasPages,
maxTextureSize: atlas.constructor.maxTextureSize
};
atlas.constructor.maxAtlasPages = 4;
atlas.constructor.maxTextureSize = 512;
return result;
});
expect(limits, 'TextureAtlas limits must be initialized').toBeDefined();
return limits!;
}
async function restoreAtlasLimits(ctx: ITestContext, limits: IAtlasLimits | undefined): Promise<void> {
if (!limits) {
return;
}
await ctx.page.evaluate(original => {
const w = window as unknown as ITestWindow;
const atlas = w.term._core?._renderService?._renderer?.value?._charAtlas;
if (atlas) {
atlas.constructor.maxAtlasPages = original.maxAtlasPages;
atlas.constructor.maxTextureSize = original.maxTextureSize;
}
}, limits);
}
async function installAtlasEventStats(ctx: ITestContext): Promise<void> {
const installed = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
const atlas = w.term._core?._renderService?._renderer?.value?._charAtlas;
if (!atlas || !w.addon) {
return false;
}
const stats: IAtlasEventStats = {
removals: 0,
maxPages: atlas.pages.length
};
w.atlasEventStats = stats;
w.addon.onAddTextureAtlasCanvas(() => {
stats.maxPages = Math.max(stats.maxPages, atlas.pages.length);
});
w.addon.onRemoveTextureAtlasCanvas(() => stats.removals++);
return true;
});
expect(installed, 'atlas event tracking must be installed').toBe(true);
}
async function getAtlasState(ctx: ITestContext): Promise<IAtlasState> {
const state = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
const renderer = w.term._core?._renderService?._renderer?.value;
const atlas = renderer?._charAtlas;
const glyphRenderer = renderer?._glyphRenderer?.value;
const stats = w.atlasEventStats;
if (!atlas || !glyphRenderer || !stats) {
return undefined;
}
return {
pages: atlas.pages.length,
atlasTextures: glyphRenderer._atlasTextures.length,
pageLayoutVersion: atlas.pageLayoutVersion,
overflowPageCreated: !!atlas._overflowSizePage,
removals: stats.removals,
maxPages: stats.maxPages
};
});
expect(state, 'atlas state must be available').toBeDefined();
return state!;
}
function generateUniqueGlyphFlood(count: number, cols: number, offset: number): string {
const base = 0x4E00;
const range = 0x9FFF - base;
const perRow = Math.max(1, Math.floor(cols / 2));
let result = '';
for (let i = 0; i < count; i++) {
result += String.fromCodePoint(base + ((offset + i) % range));
if ((i + 1) % perRow === 0 && i + 1 < count) {
result += '\r\n';
}
}
return result;
}
async function captureRowSignatures(ctx: ITestContext, selector: string, cols: number, row: number, sampleCols: number[], debugName: string): Promise<CellSignature[]> {
const screenshotOptions = process.env.DEBUG ? { path: `out-esbuild-test/playwright/${debugName}.png` } : undefined;
const buffer = await ctx.page.locator(selector).screenshot(screenshotOptions);
const frame = {
cols,
rows: 24,
decoded: (await decodePng(new Uint8Array(buffer), { force32: true })).image
};
return sampleCols.map(col => cellSignature(frame, col, row));
}
function cellSignature(frame: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): CellSignature {
const grid = 4;
const data = frame.decoded.data;
const imageWidth = frame.decoded.width;
const cellWidth = frame.decoded.width / frame.cols;
const cellHeight = frame.decoded.height / frame.rows;
const x0 = (col - 1) * cellWidth;
const y0 = (row - 1) * cellHeight;
const sums = new Array<number>(grid * grid * 4).fill(0);
const counts = new Array<number>(grid * grid).fill(0);
const startX = Math.max(0, Math.floor(x0));
const startY = Math.max(0, Math.floor(y0));
const endX = Math.min(frame.decoded.width, Math.ceil(x0 + cellWidth));
const endY = Math.min(frame.decoded.height, Math.ceil(y0 + cellHeight));
for (let y = startY; y < endY; y++) {
const gy = Math.min(grid - 1, Math.max(0, Math.floor(((y - y0) / cellHeight) * grid)));
for (let x = startX; x < endX; x++) {
const gx = Math.min(grid - 1, Math.max(0, Math.floor(((x - x0) / cellWidth) * grid)));
const sub = gy * grid + gx;
const i = (y * imageWidth + x) * 4;
sums[sub * 4] += data[i];
sums[sub * 4 + 1] += data[i + 1];
sums[sub * 4 + 2] += data[i + 2];
sums[sub * 4 + 3] += data[i + 3];
counts[sub]++;
}
}
const signature = new Array<number>(grid * grid * 4).fill(0);
for (let sub = 0; sub < counts.length; sub++) {
if (counts[sub] > 0) {
signature[sub * 4] = sums[sub * 4] / counts[sub];
signature[sub * 4 + 1] = sums[sub * 4 + 1] / counts[sub];
signature[sub * 4 + 2] = sums[sub * 4 + 2] / counts[sub];
signature[sub * 4 + 3] = sums[sub * 4 + 3] / counts[sub];
}
}
return signature;
}
function signatureDiff(actual: CellSignature, reference: CellSignature): number {
let diff = 0;
for (let i = 0; i < actual.length; i++) {
diff = Math.max(diff, Math.abs(actual[i] - reference[i]));
}
return diff;
}
function expectSignatureMatches(actual: CellSignature, reference: CellSignature, message: string): void {
const diff = signatureDiff(actual, reference);
expect(diff, `${message}; max channel diff ${diff}`).toBeLessThanOrEqual(14);
}
test.describe('atlas page overflow (#6038)', () => {
test.skip(({ browserName }) => browserName !== 'chromium');
test.describe.configure({ timeout: 90000 });
test('evicts normal pages when the page cap cannot be reduced by merging', async ({ browser }) => {
const ctx = await createTestContext(browser);
const errors: string[] = [];
const onError = (error: Error): void => { errors.push(error.message); };
let limits: IAtlasLimits | undefined;
ctx.page.on('pageerror', onError);
try {
await openTerminal(ctx, { cols: 80, rows: 24 });
await loadWebglStrict(ctx);
limits = await configureAtlasLimits(ctx);
await openTerminal(ctx, { cols: 80, rows: 24 });
await loadWebglStrict(ctx);
await createTerminalB(ctx, 80);
await installAtlasEventStats(ctx);
const atlasShared = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
return w.term._core?._renderService?._renderer?.value?._charAtlas === w.termB?._core?._renderService?._renderer?.value?._charAtlas;
});
expect(atlasShared, 'terminals with equal configs should share one texture atlas').toBe(true);
await writeToBAndWaitForRender(ctx, `\x1b[H${NORMAL_HEADER}\x1b[2;1H${POST_EVICTION_TEXT}`);
const headerReference = await captureRowSignatures(ctx, TERM_B_SELECTOR, 80, 1, NORMAL_HEADER_COLS, 'atlas-overflow-header-reference');
const postReference = await captureRowSignatures(ctx, TERM_B_SELECTOR, 80, 2, POST_EVICTION_COLS, 'atlas-overflow-post-reference');
await writeToBAndWaitForRender(ctx, '\x1b[2;1H\x1b[2K');
const initialState = await getAtlasState(ctx);
const glyphsPerChunk = 23 * 40 - 1;
for (let chunk = 0; chunk < 24; chunk++) {
await ctx.proxy.write('\x1b[H\x1b[2J' + generateUniqueGlyphFlood(glyphsPerChunk, 80, chunk * glyphsPerChunk));
await ctx.page.waitForTimeout(50);
const state = await getAtlasState(ctx);
if (state.removals > 0 || state.pages > state.atlasTextures || errors.length > 0) {
break;
}
}
const evictionState = await getAtlasState(ctx);
expect(evictionState.removals, 'normal atlas pages must be evicted at the page cap').toBeGreaterThan(0);
expect(evictionState.pageLayoutVersion, 'eviction must invalidate renderer models').toBeGreaterThan(initialState.pageLayoutVersion);
expect(evictionState.maxPages, 'atlas pages must stay within renderer texture capacity').toBeLessThanOrEqual(evictionState.atlasTextures);
expect(errors, `renderer must not throw during atlas eviction: ${errors[0] ?? ''}`).toEqual([]);
await writeToBAndWaitForRender(ctx, `\x1b[2;1H${POST_EVICTION_TEXT}`);
const termBHeader = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
return w.termB?.buffer.active.getLine(0)?.translateToString(true);
});
expect(termBHeader, 'terminal B buffer must still contain the header').toBe(NORMAL_HEADER);
const headerAfter = await captureRowSignatures(ctx, TERM_B_SELECTOR, 80, 1, NORMAL_HEADER_COLS, 'atlas-overflow-header-after');
const postAfter = await captureRowSignatures(ctx, TERM_B_SELECTOR, 80, 2, POST_EVICTION_COLS, 'atlas-overflow-post-after');
for (let i = 0; i < NORMAL_HEADER_COLS.length; i++) {
expectSignatureMatches(headerAfter[i], headerReference[i], `existing header col ${NORMAL_HEADER_COLS[i]} changed after eviction`);
}
for (let i = 0; i < POST_EVICTION_COLS.length; i++) {
expectSignatureMatches(postAfter[i], postReference[i], `new text col ${POST_EVICTION_COLS[i]} changed after eviction`);
}
const stableState = await getAtlasState(ctx);
await refreshAndWaitForRender(ctx, 'termB');
const refreshedState = await getAtlasState(ctx);
expect(refreshedState.pageLayoutVersion, 'an unchanged full refresh must not evict again').toBe(stableState.pageLayoutVersion);
expect(refreshedState.removals, 'an unchanged full refresh must not remove more pages').toBe(stableState.removals);
} finally {
ctx.page.off('pageerror', onError);
await restoreAtlasLimits(ctx, limits).catch(() => {});
await ctx.page.close();
}
});
test('evicts before adding an oversized glyph page at the page cap', async ({ browser }) => {
const ctx = await createTestContext(browser);
const errors: string[] = [];
const onError = (error: Error): void => { errors.push(error.message); };
let limits: IAtlasLimits | undefined;
ctx.page.on('pageerror', onError);
try {
await openTerminal(ctx, { cols: 160, rows: 24 });
await loadWebglStrict(ctx);
limits = await configureAtlasLimits(ctx);
await openTerminal(ctx, { cols: 160, rows: 24 });
await loadWebglStrict(ctx);
await installAtlasEventStats(ctx);
let state = await getAtlasState(ctx);
let offset = 0;
for (let chunk = 0; chunk < 120 && state.pages < state.atlasTextures; chunk++) {
const glyphCount = state.pages < state.atlasTextures - 1 ? 200 : 25;
await ctx.proxy.write('\x1b[H\x1b[2J' + generateUniqueGlyphFlood(glyphCount, 160, offset));
await ctx.page.waitForTimeout(50);
offset += glyphCount;
state = await getAtlasState(ctx);
}
expect(state.pages, 'normal atlas pages must reach the texture capacity before the oversized glyph').toBe(state.atlasTextures);
expect(state.overflowPageCreated, 'normal glyph filling must not create the oversized page').toBe(false);
expect(errors, `renderer must not fail while filling normal pages: ${errors[0] ?? ''}`).toEqual([]);
await writeAndWaitForRender(ctx, '\x1b[H\x1b[2K\x1b[24;1H');
const sampleCols = [1, 8, 16, 24, 32];
const blankSignatures = await captureRowSignatures(ctx, TERM_SELECTOR, 160, 1, sampleCols, 'atlas-overflow-wide-blank');
const joinedLength = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
const renderer = w.term._core?._renderService?._renderer?.value;
const atlas = renderer?._charAtlas;
if (!renderer || !atlas) {
throw new Error('renderer and atlas must exist');
}
const cellWidth = renderer.dimensions.device.cell.width;
const length = Math.min(w.term.cols - 1, Math.ceil(atlas._textureSize / cellWidth) + 32);
if (length * cellWidth <= atlas._textureSize) {
throw new Error('joined glyph must be wider than a normal atlas page');
}
const marker = 'W'.repeat(length);
w.term.registerCharacterJoiner(text => text.startsWith(marker) ? [[0, length]] : []);
return length;
});
const beforeOversized = await getAtlasState(ctx);
await ctx.proxy.write(`\x1b[H${'W'.repeat(joinedLength)}`);
await ctx.page.waitForTimeout(50);
const oversizedState = await getAtlasState(ctx);
expect(oversizedState.overflowPageCreated, 'the joined glyph must use the oversized atlas page').toBe(true);
expect(oversizedState.removals, 'normal pages must be evicted before adding the oversized page').toBeGreaterThan(beforeOversized.removals);
expect(oversizedState.maxPages, 'oversized page creation must stay within renderer texture capacity').toBeLessThanOrEqual(oversizedState.atlasTextures);
expect(oversizedState.pageLayoutVersion, 'oversized eviction must invalidate renderer models').toBeGreaterThan(beforeOversized.pageLayoutVersion);
expect(errors, `renderer must not throw while adding the oversized page: ${errors[0] ?? ''}`).toEqual([]);
const wideSignatures = await captureRowSignatures(ctx, TERM_SELECTOR, 160, 1, sampleCols, 'atlas-overflow-wide-after');
for (let i = 0; i < sampleCols.length; i++) {
expect(signatureDiff(wideSignatures[i], blankSignatures[i]), `wide glyph col ${sampleCols[i]} must render visible pixels`).toBeGreaterThan(14);
}
const stableState = await getAtlasState(ctx);
await refreshAndWaitForRender(ctx, 'term');
const refreshedState = await getAtlasState(ctx);
expect(refreshedState.pageLayoutVersion, 'oversized glyph refresh must settle without another eviction').toBe(stableState.pageLayoutVersion);
expect(refreshedState.removals, 'oversized glyph refresh must not remove more pages').toBe(stableState.removals);
const wideAfterRefresh = await captureRowSignatures(ctx, TERM_SELECTOR, 160, 1, sampleCols, 'atlas-overflow-wide-refreshed');
for (let i = 0; i < sampleCols.length; i++) {
expectSignatureMatches(wideAfterRefresh[i], wideSignatures[i], `wide glyph col ${sampleCols[i]} changed after refresh`);
}
} finally {
ctx.page.off('pageerror', onError);
await restoreAtlasLimits(ctx, limits).catch(() => {});
await ctx.page.close();
}
});
});
@@ -1,288 +0,0 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { decodePng, type IImage32 } from '@lunapaint/png-codec';
import test, { expect } from '@playwright/test';
import type { Terminal, ITerminalInitOnlyOptions, ITerminalOptions } from '@xterm/xterm';
import type { IWebglAddonOptions, WebglAddon } from '@xterm/addon-webgl';
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
type CellSignature = number[];
type TestTerminalConstructor = new (options?: ITerminalOptions & ITerminalInitOnlyOptions) => ITestTerminal;
type TestWebglAddonConstructor = new (options?: IWebglAddonOptions) => ITestWebglAddon;
interface ITestTextureAtlas {
readonly pages: ReadonlyArray<{ readonly canvas: HTMLCanvasElement }>;
}
interface ITestGlyphRenderer {
readonly _atlasTextures: ReadonlyArray<unknown>;
}
interface ITestRenderer {
_charAtlas?: ITestTextureAtlas;
_glyphRenderer?: {
value?: ITestGlyphRenderer;
};
}
interface ITestRenderService {
_renderer?: {
value?: ITestRenderer;
};
}
interface ITestTerminal extends Terminal {
_core?: {
_renderService?: ITestRenderService;
};
}
interface ITestWebglAddon extends WebglAddon {
_renderer?: ITestRenderer;
}
interface IAtlasEventStats {
additions: number;
maxPages: number;
}
interface ITestWindow {
WebglAddon: TestWebglAddonConstructor;
term: ITestTerminal;
addon?: ITestWebglAddon;
atlasEventStats?: IAtlasEventStats;
}
interface IAtlasState {
additions: number;
atlasTextures: number;
maxPages: number;
}
const REFERENCE_LINE = 'Ref0123456789ABCDEF';
const REFERENCE_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
const TERM_SELECTOR = '#terminal-container .xterm-screen';
async function loadWebglStrict(ctx: ITestContext): Promise<void> {
await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
w.addon = new w.WebglAddon({ preserveDrawingBuffer: true });
w.term.loadAddon(w.addon);
});
const isWebglRenderer = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
return !!w.addon && w.term._core?._renderService?._renderer?.value === w.addon._renderer;
});
expect(isWebglRenderer, 'WebGL renderer must be active').toBe(true);
}
async function installAtlasEventStats(ctx: ITestContext): Promise<void> {
const installed = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
const atlas = w.term._core?._renderService?._renderer?.value?._charAtlas;
if (!atlas || !w.addon) {
return false;
}
const stats: IAtlasEventStats = {
additions: 0,
maxPages: atlas.pages.length
};
w.atlasEventStats = stats;
w.addon.onAddTextureAtlasCanvas(() => {
stats.additions++;
stats.maxPages = Math.max(stats.maxPages, atlas.pages.length);
});
return true;
});
expect(installed, 'atlas event tracking must be installed').toBe(true);
}
async function getAtlasState(ctx: ITestContext): Promise<IAtlasState> {
const state = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
const renderer = w.term._core?._renderService?._renderer?.value;
const glyphRenderer = renderer?._glyphRenderer?.value;
const stats = w.atlasEventStats;
if (!glyphRenderer || !stats) {
return undefined;
}
return {
additions: stats.additions,
atlasTextures: glyphRenderer._atlasTextures.length,
maxPages: stats.maxPages
};
});
expect(state, 'atlas state must be available').toBeDefined();
return state!;
}
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
const renderPromise = new Promise<void>(resolve => {
const disposable = ctx.proxy.onRender(() => {
disposable.dispose();
resolve();
});
});
await ctx.proxy.write(data);
await renderPromise;
}
async function refreshAndWaitForRender(ctx: ITestContext): Promise<void> {
const renderPromise = new Promise<void>(resolve => {
const disposable = ctx.proxy.onRender(() => {
disposable.dispose();
resolve();
});
});
await ctx.proxy.refresh(0, (await ctx.proxy.rows) - 1);
await renderPromise;
}
function generateSyntheticTuiFrames(frameCount: number, cols: number, rows: number): string[] {
const spinner = ['|', '/', '-', '\\'];
const words = ['reading', 'rendering', 'atlas', 'terminal', 'buffer', 'tokens', 'diff', 'patch'];
const frames: string[] = [];
for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) {
const lines = [
`+${'-'.repeat(cols - 2)}+`,
`|${(` ${spinner[frameIndex % spinner.length]} Copilot is working`).padEnd(cols - 2).slice(0, cols - 2)}|`,
`+${'-'.repeat(cols - 2)}+`
];
for (let row = 3; row < rows - 1; row++) {
const foreground = 31 + ((frameIndex + row) % 6);
const text = `${words[(frameIndex + row) % words.length]} ${frameIndex}:${row}`;
lines.push(`\x1b[${foreground}m${text}\x1b[0m`);
}
lines.push(`\x1b[2mtokens ${1000 + frameIndex * 37}\x1b[0m`);
frames.push(`\x1b[2J\x1b[H${lines.join('\r\n')}`);
}
return frames;
}
function generateUniqueGlyphFlood(count: number, cols: number, offset: number): string {
const base = 0x4E00;
const range = 0x9FFF - base;
const perRow = Math.max(1, Math.floor(cols / 2));
let result = '';
for (let i = 0; i < count; i++) {
result += String.fromCodePoint(base + ((offset + i) % range));
if ((i + 1) % perRow === 0 && i + 1 < count) {
result += '\r\n';
}
}
return result;
}
async function captureRowSignatures(ctx: ITestContext, row: number, cols: number[], debugName: string): Promise<CellSignature[]> {
const screenshotOptions = process.env.DEBUG ? { path: `out-esbuild-test/playwright/${debugName}.png` } : undefined;
const buffer = await ctx.page.locator(TERM_SELECTOR).screenshot(screenshotOptions);
const frame = {
cols: await ctx.proxy.cols,
rows: await ctx.proxy.rows,
decoded: (await decodePng(new Uint8Array(buffer), { force32: true })).image
};
return cols.map(col => cellSignature(frame, col, row));
}
function cellSignature(frame: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): CellSignature {
const grid = 4;
const data = frame.decoded.data;
const imageWidth = frame.decoded.width;
const cellWidth = frame.decoded.width / frame.cols;
const cellHeight = frame.decoded.height / frame.rows;
const x0 = (col - 1) * cellWidth;
const y0 = (row - 1) * cellHeight;
const sums = new Array<number>(grid * grid * 4).fill(0);
const counts = new Array<number>(grid * grid).fill(0);
const startX = Math.max(0, Math.floor(x0));
const startY = Math.max(0, Math.floor(y0));
const endX = Math.min(frame.decoded.width, Math.ceil(x0 + cellWidth));
const endY = Math.min(frame.decoded.height, Math.ceil(y0 + cellHeight));
for (let y = startY; y < endY; y++) {
const gy = Math.min(grid - 1, Math.max(0, Math.floor(((y - y0) / cellHeight) * grid)));
for (let x = startX; x < endX; x++) {
const gx = Math.min(grid - 1, Math.max(0, Math.floor(((x - x0) / cellWidth) * grid)));
const sub = gy * grid + gx;
const i = (y * imageWidth + x) * 4;
sums[sub * 4] += data[i];
sums[sub * 4 + 1] += data[i + 1];
sums[sub * 4 + 2] += data[i + 2];
sums[sub * 4 + 3] += data[i + 3];
counts[sub]++;
}
}
const signature = new Array<number>(grid * grid * 4).fill(0);
for (let sub = 0; sub < counts.length; sub++) {
if (counts[sub] > 0) {
signature[sub * 4] = sums[sub * 4] / counts[sub];
signature[sub * 4 + 1] = sums[sub * 4 + 1] / counts[sub];
signature[sub * 4 + 2] = sums[sub * 4 + 2] / counts[sub];
signature[sub * 4 + 3] = sums[sub * 4 + 3] / counts[sub];
}
}
return signature;
}
function expectSignatureMatches(actual: CellSignature, reference: CellSignature, message: string): void {
let diff = 0;
for (let i = 0; i < actual.length; i++) {
diff = Math.max(diff, Math.abs(actual[i] - reference[i]));
}
expect(diff, `${message}; max channel diff ${diff}`).toBeLessThanOrEqual(14);
}
test.describe('WebGL atlas TUI stress (#6038)', () => {
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
test.describe.configure({ timeout: 90000 });
test('primary-buffer glyphs survive alternate-buffer redraws and atlas page churn', async ({ browser }) => {
const ctx = await createTestContext(browser);
const errors: string[] = [];
const onError = (error: Error): void => { errors.push(error.message); };
ctx.page.on('pageerror', onError);
try {
await openTerminal(ctx, { cols: 80, rows: 24, scrollback: 5000 });
await loadWebglStrict(ctx);
await installAtlasEventStats(ctx);
await writeAndWaitForRender(ctx, REFERENCE_LINE);
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS, 'atlas-stress-reference');
await writeAndWaitForRender(ctx, '\x1b[?1049h');
for (const frame of generateSyntheticTuiFrames(30, 80, 24)) {
await writeAndWaitForRender(ctx, frame);
}
const glyphsPerViewport = 23 * 40 - 1;
for (let chunk = 0; chunk < 4; chunk++) {
await writeAndWaitForRender(ctx, `\x1b[2J\x1b[H${generateUniqueGlyphFlood(glyphsPerViewport, 80, chunk * glyphsPerViewport)}`);
}
await writeAndWaitForRender(ctx, '\x1b[?1049l');
await refreshAndWaitForRender(ctx);
const state = await getAtlasState(ctx);
expect(state.additions, 'stress workload must add atlas pages').toBeGreaterThan(0);
expect(state.maxPages, 'stress workload must exercise multiple atlas pages').toBeGreaterThan(1);
expect(state.maxPages, 'atlas pages must remain within renderer texture capacity').toBeLessThanOrEqual(state.atlasTextures);
expect(errors, `renderer must not throw during TUI stress: ${errors[0] ?? ''}`).toEqual([]);
const primaryText = await ctx.page.evaluate(() => {
const w = window as unknown as ITestWindow;
return w.term.buffer.active.getLine(0)?.translateToString(true);
});
expect(primaryText, 'primary buffer must still contain the reference line').toBe(REFERENCE_LINE);
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS, 'atlas-stress-after');
for (let i = 0; i < REFERENCE_COLS.length; i++) {
expectSignatureMatches(after[i], reference[i], `primary-buffer glyph at col ${REFERENCE_COLS[i]} changed after TUI stress`);
}
} finally {
ctx.page.off('pageerror', onError);
await ctx.page.close();
}
});
});
@@ -1,77 +0,0 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import test, { expect } from '@playwright/test';
import { platform } from 'os';
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
interface ITestRendererWithAtlasCanvas {
readonly _charAtlas?: {
readonly _tmpCanvas?: HTMLCanvasElement;
};
}
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
const renderPromise = new Promise<void>(resolve => {
const disposable = ctx.proxy.onRender(() => {
disposable.dispose();
resolve();
});
});
await ctx.proxy.write(data);
await renderPromise;
}
test.describe('WebGL custom glyphs', () => {
if (platform() === 'linux') {
test.skip(({ browserName }) => browserName === 'firefox' || browserName === 'webkit');
}
test('pattern glyphs render after the terminal is adopted into another document', async ({ browser }) => {
const ctx = await createTestContext(browser);
const errors: string[] = [];
const onError = (error: Error): void => { errors.push(error.message); };
ctx.page.on('pageerror', onError);
try {
await openTerminal(ctx);
await ctx.page.evaluate(`
window.addon = new window.WebglAddon({ customGlyphs: true });
window.term.loadAddon(window.addon);
`);
await ctx.page.evaluate(() => {
const frame = document.createElement('iframe');
document.body.appendChild(frame);
const auxiliaryDocument = frame.contentDocument;
if (!auxiliaryDocument?.body || !window.term.element) {
throw new Error('Auxiliary document and terminal element must be available');
}
const renderer = window.term._core?._renderService?._renderer?.value as ITestRendererWithAtlasCanvas | undefined;
const atlasCanvas = renderer?._charAtlas?._tmpCanvas;
if (!atlasCanvas) {
throw new Error('Texture atlas canvas must be available');
}
// Model the atlas state after an uncached glyph has attached the measurement canvas.
window.term.element.appendChild(atlasCanvas);
auxiliaryDocument.body.appendChild(window.term.element);
if (atlasCanvas.ownerDocument !== auxiliaryDocument) {
throw new Error('Texture atlas canvas must be adopted into the auxiliary document');
}
auxiliaryDocument.createElement = () => {
throw new Error('Not allowed to create elements in the auxiliary document');
};
});
await writeAndWaitForRender(ctx, '\u2591');
await writeAndWaitForRender(ctx, 'X');
const line = await ctx.page.evaluate(() => window.term.buffer.active.getLine(0)?.translateToString(true));
expect(errors, `renderer must not create pattern canvases in the auxiliary document: ${errors[0] ?? ''}`).toEqual([]);
expect(line).toBe('\u2591X');
} finally {
ctx.page.off('pageerror', onError);
await ctx.page.close();
}
});
});
@@ -1,282 +0,0 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IImage32, decodePng } from '@lunapaint/png-codec';
import test, { expect } from '@playwright/test';
import type { Terminal, ITerminalInitOnlyOptions, ITerminalOptions } from '@xterm/xterm';
import type { IWebglAddonOptions, WebglAddon } from '@xterm/addon-webgl';
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
type CellSignature = number[];
type TestTerminalConstructor = new (options?: ITerminalOptions & ITerminalInitOnlyOptions) => ITestTerminal;
type TestWebglAddonConstructor = new (options?: IWebglAddonOptions) => ITestWebglAddon;
interface ITestTextureAtlasConstructor {
maxAtlasPages: number | undefined;
}
interface ITestTextureAtlas {
constructor: ITestTextureAtlasConstructor;
}
interface ITestRenderer {
_charAtlas?: ITestTextureAtlas;
}
interface ITestRenderService {
_renderer?: {
value?: ITestRenderer;
};
}
interface ITestTerminal extends Terminal {
_core?: {
_renderService?: ITestRenderService;
};
}
interface ITestWebglAddon extends WebglAddon {
_renderer?: ITestRenderer;
}
declare global {
interface Window { // eslint-disable-line @typescript-eslint/naming-convention
Terminal: TestTerminalConstructor;
WebglAddon: TestWebglAddonConstructor;
term: ITestTerminal;
termB?: ITestTerminal;
addon?: ITestWebglAddon;
addonB?: ITestWebglAddon;
}
}
const HEADER = ' HEADER_REF_0123456789';
const HEADER_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const TERM_B_SELECTOR = '#terminal-container-b .xterm-screen';
async function loadWebglStrict(ctx: ITestContext): Promise<void> {
await ctx.page.evaluate(() => {
window.addon = new window.WebglAddon({ preserveDrawingBuffer: true });
window.term.loadAddon(window.addon);
});
const isWebglRenderer = await ctx.page.evaluate(() => {
return !!window.addon && window.term?._core?._renderService?._renderer?.value === window.addon._renderer;
});
expect(isWebglRenderer, 'WebGL renderer must be active').toBe(true);
}
async function createTerminalB(ctx: ITestContext): Promise<void> {
await ctx.page.evaluate(() => {
window.termB?.dispose();
document.getElementById('terminal-container-b')?.remove();
const el = document.createElement('div');
el.id = 'terminal-container-b';
document.body.appendChild(el);
window.termB = new window.Terminal({ cols: 80, rows: 24, allowProposedApi: true });
window.termB.open(el);
window.addonB = new window.WebglAddon({ preserveDrawingBuffer: true });
window.termB.loadAddon(window.addonB);
});
}
async function writeToBAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
await ctx.page.evaluate(d => new Promise<void>(resolve => {
const termB = window.termB;
if (!termB) {
throw new Error('Terminal B must be created before writing to it');
}
const disposable = termB.onRender(() => {
disposable.dispose();
resolve();
});
termB.write(d);
}), data);
}
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
const renderPromise = new Promise<void>(resolve => {
const disposable = ctx.proxy.onRender(() => {
disposable.dispose();
resolve();
});
});
await ctx.proxy.write(data);
await renderPromise;
}
async function setMaxAtlasPages(ctx: ITestContext, maxAtlasPages: number): Promise<void> {
const applied = await ctx.page.evaluate(max => {
const atlas = window.term?._core?._renderService?._renderer?.value?._charAtlas;
if (!atlas) {
return false;
}
atlas.constructor.maxAtlasPages = max;
return true;
}, maxAtlasPages);
expect(applied, 'should be able to set TextureAtlas.maxAtlasPages').toBe(true);
}
async function resetMaxAtlasPages(ctx: ITestContext): Promise<void> {
await ctx.page.evaluate(() => {
const atlas = window.term?._core?._renderService?._renderer?.value?._charAtlas;
if (atlas) {
atlas.constructor.maxAtlasPages = undefined;
}
});
}
function generateColoredAsciiFlood(cells: number, offset: number = 0): string {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let out = '';
for (let i = 0; i < cells; i++) {
const ch = letters[(offset + i) % letters.length];
const fg = (offset + i) % 256;
const bg = (offset + i * 7) % 256;
out += `\x1b[38;5;${fg}m\x1b[48;5;${bg}m${ch}`;
if ((i + 1) % 78 === 0 && i + 1 < cells) {
out += '\x1b[0m\r\n';
}
}
return out + '\x1b[0m';
}
async function captureRowSignatures(ctx: ITestContext, row: number, cols: number[]): Promise<CellSignature[]> {
const screenshotOptions = process.env.DEBUG ? { path: 'out-esbuild-test/playwright/shared-atlas-term-b.png' } : undefined;
const buffer = await ctx.page.locator(TERM_B_SELECTOR).screenshot(screenshotOptions);
const frame = {
cols: 80,
rows: 24,
decoded: (await decodePng(new Uint8Array(buffer), { force32: true })).image
};
return cols.map(col => cellSignature(frame, col, row));
}
function cellSignature(frame: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): CellSignature {
const grid = 4;
const data = frame.decoded.data;
const imageWidth = frame.decoded.width;
const cellWidth = frame.decoded.width / frame.cols;
const cellHeight = frame.decoded.height / frame.rows;
const x0 = (col - 1) * cellWidth;
const y0 = (row - 1) * cellHeight;
const sums = new Array<number>(grid * grid * 4).fill(0);
const counts = new Array<number>(grid * grid).fill(0);
const startX = Math.max(0, Math.floor(x0));
const startY = Math.max(0, Math.floor(y0));
const endX = Math.min(frame.decoded.width, Math.ceil(x0 + cellWidth));
const endY = Math.min(frame.decoded.height, Math.ceil(y0 + cellHeight));
for (let y = startY; y < endY; y++) {
const gy = Math.min(grid - 1, Math.max(0, Math.floor(((y - y0) / cellHeight) * grid)));
for (let x = startX; x < endX; x++) {
const gx = Math.min(grid - 1, Math.max(0, Math.floor(((x - x0) / cellWidth) * grid)));
const sub = gy * grid + gx;
const i = (y * imageWidth + x) * 4;
sums[sub * 4] += data[i];
sums[sub * 4 + 1] += data[i + 1];
sums[sub * 4 + 2] += data[i + 2];
sums[sub * 4 + 3] += data[i + 3];
counts[sub]++;
}
}
const signature = new Array<number>(grid * grid * 4).fill(0);
for (let sub = 0; sub < counts.length; sub++) {
if (counts[sub] > 0) {
signature[sub * 4] = sums[sub * 4] / counts[sub];
signature[sub * 4 + 1] = sums[sub * 4 + 1] / counts[sub];
signature[sub * 4 + 2] = sums[sub * 4 + 2] / counts[sub];
signature[sub * 4 + 3] = sums[sub * 4 + 3] / counts[sub];
}
}
return signature;
}
function expectSignatureMatches(actual: CellSignature, reference: CellSignature, message: string): void {
let diff = 0;
for (let i = 0; i < actual.length; i++) {
diff = Math.max(diff, Math.abs(actual[i] - reference[i]));
}
expect(diff, `${message}; max channel diff ${diff}`).toBeLessThanOrEqual(14);
}
test.describe('shared-atlas garble across terminals (#6038)', () => {
test.skip(({ browserName }) => browserName !== 'chromium');
test.describe.configure({ timeout: 60000 });
test('keeps a second terminal rendering correctly after shared atlas page merges', async ({ browser }) => {
const ctx = await createTestContext(browser);
try {
await openTerminal(ctx, { cols: 80, rows: 24 });
await loadWebglStrict(ctx);
await createTerminalB(ctx);
const atlasShared = await ctx.page.evaluate(() => {
return window.term._core?._renderService?._renderer?.value?._charAtlas === window.termB?._core?._renderService?._renderer?.value?._charAtlas;
});
expect(atlasShared, 'terminals with equal configs should share one texture atlas').toBe(true);
await setMaxAtlasPages(ctx, 4);
await writeToBAndWaitForRender(ctx, HEADER);
const reference = await captureRowSignatures(ctx, 1, HEADER_COLS);
for (let c = 0; c < 10; c++) {
await writeAndWaitForRender(ctx, '\x1b[2;1H' + generateColoredAsciiFlood(23 * 78, c * 23 * 78));
}
await writeToBAndWaitForRender(ctx, '\x1b[24;1Hx');
const termBHeader = await ctx.page.evaluate(() => window.termB?.buffer.active.getLine(0)?.translateToString(true));
expect(termBHeader, 'terminal B buffer must still contain the header').toBe(HEADER);
const after = await captureRowSignatures(ctx, 1, HEADER_COLS);
for (let i = 0; i < HEADER_COLS.length; i++) {
expectSignatureMatches(after[i], reference[i], `terminal B header col ${HEADER_COLS[i]} garbled by terminal A's atlas merges`);
}
} finally {
await resetMaxAtlasPages(ctx).catch(() => {});
await ctx.page.close();
}
});
test('keeps a second terminal rendering correctly after terminal A clears the shared atlas', async ({ browser }) => {
const ctx = await createTestContext(browser);
try {
await openTerminal(ctx, { cols: 80, rows: 24 });
await loadWebglStrict(ctx);
await createTerminalB(ctx);
const atlasShared = await ctx.page.evaluate(() => {
return window.term._core?._renderService?._renderer?.value?._charAtlas === window.termB?._core?._renderService?._renderer?.value?._charAtlas;
});
expect(atlasShared, 'terminals with equal configs should share one texture atlas').toBe(true);
await writeToBAndWaitForRender(ctx, HEADER);
const reference = await captureRowSignatures(ctx, 1, HEADER_COLS);
// Terminal A alone clears the shared atlas, as embedders do to force a redraw (VS Code calls
// this for every terminal on OS resume). This wipes the pages that terminal B's model still
// holds texture coords into.
await ctx.proxy.clearTextureAtlas();
// Terminal A repopulates the cleared pages with a different glyph set, so B's stale coords now
// resolve to whatever A rasterized into those slots.
await writeAndWaitForRender(ctx, '\x1b[2;1H' + generateColoredAsciiFlood(23 * 78));
// Nudge B into a render that does not touch row 1, mirroring a terminal that is idle but
// visible. Row 1 must still be drawn from a model B rebuilt against the current atlas.
await writeToBAndWaitForRender(ctx, '\x1b[24;1Hx');
const termBHeader = await ctx.page.evaluate(() => window.termB?.buffer.active.getLine(0)?.translateToString(true));
expect(termBHeader, 'terminal B buffer must still contain the header').toBe(HEADER);
const after = await captureRowSignatures(ctx, 1, HEADER_COLS);
for (let i = 0; i < HEADER_COLS.length; i++) {
expectSignatureMatches(after[i], reference[i], `terminal B header col ${HEADER_COLS[i]} garbled by terminal A's atlas clear`);
}
} finally {
await ctx.page.close();
}
});
});
@@ -67,8 +67,6 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
dlIip.appendChild(dtIip); dlIip.appendChild(dtIip);
this._addDdWithButton(dlIip, 'image-demo3', 'palette (png File)'); this._addDdWithButton(dlIip, 'image-demo3', 'palette (png File)');
this._addDdWithButton(dlIip, 'image-demo4', 'dice (qoi MultipartFile)'); this._addDdWithButton(dlIip, 'image-demo4', 'dice (qoi MultipartFile)');
this._addDdWithButton(dlIip, 'image-demo5', 'rose (webp File)');
this._addDdWithButton(dlIip, 'image-demo6', 'kimono (avif MultipartFile)');
container.appendChild(dlIip); container.appendChild(dlIip);
// Kitty demos // Kitty demos
@@ -175,15 +173,11 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
document.getElementById('image-demo2')!.addEventListener('click', document.getElementById('image-demo2')!.addEventListener('click',
sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel')); sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
document.getElementById('image-demo3')!.addEventListener('click', document.getElementById('image-demo3')!.addEventListener('click',
iipDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/palette.png')); iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
document.getElementById('image-demo4')!.addEventListener('click', document.getElementById('image-demo4')!.addEventListener('click',
iipDemoMulti('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/dice.qoi')); iipDemoMulti('https://raw.githubusercontent.com/jerch/xterm-wasm-parts/master/fixtures/qoi/dice.qoi'));
document.getElementById('image-demo5')!.addEventListener('click',
iipDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/1_webp_a.webp'));
document.getElementById('image-demo6')!.addEventListener('click',
iipDemoMulti('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/kimono.crop.avif'));
document.getElementById('image-demo-kitty1')!.addEventListener('click', document.getElementById('image-demo-kitty1')!.addEventListener('click',
kittyDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/palette.png')); kittyDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
// demo for image retrieval API // demo for image retrieval API
this._terminal.element!.addEventListener('click', (ev: MouseEvent) => { this._terminal.element!.addEventListener('click', (ev: MouseEvent) => {
@@ -203,8 +197,11 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
const pos = (this._terminal as any)._core._mouseCoordsService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows); const pos = (this._terminal as any)._core._mouseCoordsService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
const x = pos[0] - 1; const x = pos[0] - 1;
const y = pos[1] - 1; const y = pos[1] - 1;
// ctrl+click: get original image const canvas = ev.shiftKey
const canvas = imageAddon.getImageAtBufferCell(x, this._terminal.buffer.active.viewportY + y); // ctrl+shift+click: get single tile
? imageAddon.extractTileAtBufferCell(x, this._terminal.buffer.active.viewportY + y)
// ctrl+click: get original image
: imageAddon.getImageAtBufferCell(x, this._terminal.buffer.active.viewportY + y);
canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank')); canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank'));
}); });
} }
@@ -7,7 +7,7 @@ import { BaseWindow } from './baseWindow';
import type { IControlWindow } from '../controlBar'; import type { IControlWindow } from '../controlBar';
import type { IBufferCell } from '@xterm/xterm'; import type { IBufferCell } from '@xterm/xterm';
// Underline style values from common/buffer/Constants.ts // Underline style values from common/primitives/buffer/Constants.ts
const enum UnderlineStyle { const enum UnderlineStyle {
NONE = 0, NONE = 0,
SINGLE = 1, SINGLE = 1,
+15 -17
View File
@@ -12,33 +12,31 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
private _textureAtlasContainer!: HTMLElement; private _textureAtlasContainer!: HTMLElement;
private _stressRunning = false; private _stressRunning = false;
private _scrollOutput: HTMLInputElement | undefined; private _stressScrolling: HTMLInputElement | undefined;
public build(container: HTMLElement): void { public build(container: HTMLElement): void {
const stressButtons = document.createElement('div'); const stressDiv = document.createElement('div');
const stressStart = document.createElement('button'); const stressStart = document.createElement('button');
stressStart.id = 'stress-start'; stressStart.id = 'stress-start';
stressStart.textContent = 'atlas stress start'; stressStart.textContent = 'atlas stress start';
stressStart.addEventListener('click', () => this._stress()); stressStart.addEventListener('click', () => this._stress());
stressButtons.appendChild(stressStart); stressDiv.appendChild(stressStart);
const stressStop = document.createElement('button'); const stressStop = document.createElement('button');
stressStop.id = 'stress-stop'; stressStop.id = 'stress-stop';
stressStop.textContent = 'atlas stress stop'; stressStop.textContent = 'atlas stress stop';
stressStop.addEventListener('click', () => this._stressRunning = false); stressStop.addEventListener('click', () => this._stressRunning = false);
stressButtons.appendChild(stressStop); stressDiv.appendChild(stressStop);
container.appendChild(stressButtons);
const stressOptions = document.createElement('div'); this._stressScrolling = document.createElement('input');
this._scrollOutput = document.createElement('input'); this._stressScrolling.type = 'checkbox';
this._scrollOutput.type = 'checkbox'; this._stressScrolling.id = 'stress-scrolling';
this._scrollOutput.id = 'scroll-output'; stressDiv.appendChild(this._stressScrolling);
stressOptions.appendChild(this._scrollOutput); const stressScrollingLabel = document.createElement('label');
const scrollOutputLabel = document.createElement('label'); stressScrollingLabel.htmlFor = 'stress-scrolling';
scrollOutputLabel.htmlFor = 'scroll-output'; stressScrollingLabel.textContent = 'stress scrolling';
scrollOutputLabel.textContent = 'Scroll output'; stressDiv.appendChild(stressScrollingLabel);
stressOptions.appendChild(scrollOutputLabel); container.appendChild(stressDiv);
container.appendChild(stressOptions);
const zoomCheckbox = document.createElement('input'); const zoomCheckbox = document.createElement('input');
zoomCheckbox.type = 'checkbox'; zoomCheckbox.type = 'checkbox';
@@ -82,7 +80,7 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
return; return;
} }
this._stressRunning = true; this._stressRunning = true;
const scrollOutput = !!(this._scrollOutput?.checked); const scrolling = !!(this._stressScrolling?.checked);
for (let r = 0; r < 256; r += 2) { for (let r = 0; r < 256; r += 2) {
for (let g = 0; g < 256; g += 2) { for (let g = 0; g < 256; g += 2) {
let s: string[] = []; let s: string[] = [];
@@ -93,7 +91,7 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
const rbg = `RGB: ${[r, g, b]}`; const rbg = `RGB: ${[r, g, b]}`;
s.push(`\r\x1b[38;2;${r};${g};${b}m${rbg.padEnd(18, ' ')}${TEXT}`); s.push(`\r\x1b[38;2;${r};${g};${b}m${rbg.padEnd(18, ' ')}${TEXT}`);
if (s.length >= 16) { if (s.length >= 16) {
if (scrollOutput) { if (scrolling) {
await new Promise<void>(r => this._terminal.write(s.join('\r\n') + '\r\n', r)); await new Promise<void>(r => this._terminal.write(s.join('\r\n') + '\r\n', r));
} else { } else {
await new Promise<void>(r => this._terminal.write('\x1b[H' + s.join('\r\n'), r)); await new Promise<void>(r => this._terminal.write('\x1b[H' + s.join('\r\n'), r));
+136 -136
View File
@@ -663,9 +663,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -680,9 +680,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -697,9 +697,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -714,9 +714,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -731,9 +731,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -748,9 +748,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -765,9 +765,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -782,9 +782,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -799,9 +799,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -816,9 +816,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -833,9 +833,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -850,9 +850,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -867,9 +867,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -884,9 +884,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -901,9 +901,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -918,9 +918,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -935,9 +935,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -952,9 +952,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -969,9 +969,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -986,9 +986,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1003,9 +1003,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1020,9 +1020,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1037,9 +1037,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1054,9 +1054,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1071,9 +1071,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1088,9 +1088,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3061,16 +3061,16 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "5.0.9", "version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^4.0.2" "balanced-match": "^4.0.2"
}, },
"engines": { "engines": {
"node": "20 || >=22" "node": "18 || 20 || >=22"
} }
}, },
"node_modules/braces": { "node_modules/braces": {
@@ -3519,15 +3519,15 @@
} }
}, },
"node_modules/concurrently": { "node_modules/concurrently": {
"version": "9.2.4", "version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
"integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "4.1.2", "chalk": "4.1.2",
"rxjs": "7.8.2", "rxjs": "7.8.2",
"shell-quote": "1.9.0", "shell-quote": "1.8.3",
"supports-color": "8.1.1", "supports-color": "8.1.1",
"tree-kill": "1.2.2", "tree-kill": "1.2.2",
"yargs": "17.7.2" "yargs": "17.7.2"
@@ -3869,9 +3869,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -3882,32 +3882,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1", "@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.1", "@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.1", "@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.1", "@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.1", "@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.1", "@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.1", "@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.1", "@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.1", "@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.1", "@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.1" "@esbuild/win32-x64": "0.28.0"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@@ -4308,9 +4308,9 @@
} }
}, },
"node_modules/express-ws/node_modules/ws": { "node_modules/express-ws/node_modules/ws": {
"version": "7.5.13", "version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4351,9 +4351,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.5", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4749,9 +4749,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/glob/node_modules/brace-expansion": { "node_modules/glob/node_modules/brace-expansion": {
"version": "2.1.4", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -5795,9 +5795,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/mocha/node_modules/brace-expansion": { "node_modules/mocha/node_modules/brace-expansion": {
"version": "2.1.4", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7127,9 +7127,9 @@
} }
}, },
"node_modules/shell-quote": { "node_modules/shell-quote": {
"version": "1.9.0", "version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -7975,9 +7975,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "7.29.0", "version": "7.25.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -8452,9 +8452,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.21.3", "version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
+2 -2
View File
@@ -6,9 +6,9 @@
import * as Strings from './LocalizableStrings'; import * as Strings from './LocalizableStrings';
import { ITerminal, IRenderDebouncer } from './Types'; import { ITerminal, IRenderDebouncer } from './Types';
import { TimeBasedDebouncer } from './TimeBasedDebouncer'; import { TimeBasedDebouncer } from './TimeBasedDebouncer';
import { Disposable, toDisposable } from '../common/Lifecycle'; import { Disposable, toDisposable } from '../common/primitives/Lifecycle';
import { ICoreBrowserService, IRenderService } from './services/Services'; import { ICoreBrowserService, IRenderService } from './services/Services';
import { IBuffer } from '../common/buffer/Types'; import { IBuffer } from '../common/primitives/buffer/Types';
import { IInstantiationService } from '../common/services/Services'; import { IInstantiationService } from '../common/services/Services';
import { addDisposableListener } from './Dom'; import { addDisposableListener } from './Dom';
+2 -2
View File
@@ -4,8 +4,8 @@
*/ */
import { IColorContrastCache } from './Types'; import { IColorContrastCache } from './Types';
import { IColor } from '../common/Types'; import { IColor } from '../common/primitives/Types';
import { TwoKeyMap } from '../common/MultiKeyMap'; import { TwoKeyMap } from '../common/primitives/MultiKeyMap';
export class ColorContrastCache implements IColorContrastCache { export class ColorContrastCache implements IColorContrastCache {
private _color: TwoKeyMap</* bg */number, /* fg */number, IColor | null> = new TwoKeyMap(); private _color: TwoKeyMap</* bg */number, /* fg */number, IColor | null> = new TwoKeyMap();
+14 -14
View File
@@ -43,22 +43,23 @@ import { SelectionService } from './services/SelectionService';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from './services/Services'; import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from './services/Services';
import { ThemeService } from './services/ThemeService'; import { ThemeService } from './services/ThemeService';
import { KeyboardService } from './services/KeyboardService'; import { KeyboardService } from './services/KeyboardService';
import { channels, color, rgb } from '../common/Color'; import { channels, color, rgb } from '../common/primitives/Color';
import { CoreTerminal } from '../common/CoreTerminal'; import { CoreTerminal } from '../common/CoreTerminal';
import * as Browser from '../common/Platform'; import * as Browser from '../common/primitives/Platform';
import { ColorRequestType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from '../common/Types'; import { ColorRequestType, IColorEvent, KeyboardResultType, SpecialColorIndex } from '../common/primitives/Types';
import { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine'; import { ITerminalOptions } from '../common/Types';
import { IBuffer } from '../common/buffer/Types'; import { DEFAULT_ATTR_DATA } from '../common/primitives/buffer/BufferLine';
import { C0, C1ESCAPED } from '../common/data/EscapeSequences'; import { IBuffer } from '../common/primitives/buffer/Types';
import { toRgbString } from '../common/input/XParseColor'; import { C0, C1ESCAPED } from '../common/primitives/data/EscapeSequences';
import { toRgbString } from '../common/primitives/input/XParseColor';
import { DecorationService } from '../common/services/DecorationService'; import { DecorationService } from '../common/services/DecorationService';
import { IDecorationService } from '../common/services/Services'; import { IDecorationService } from '../common/services/Services';
import { WindowsOptionsReportType } from '../common/InputHandler'; import { WindowsOptionsReportType } from '../common/InputHandler';
import { AccessibilityManager } from './AccessibilityManager'; import { AccessibilityManager } from './AccessibilityManager';
import { Linkifier } from './Linkifier'; import { Linkifier } from './Linkifier';
import { Emitter, EventUtils, type IEvent } from '../common/Event'; import { Emitter, EventUtils, type IEvent } from '../common/primitives/Event';
import { addDisposableListener } from './Dom'; import { addDisposableListener } from './Dom';
import { MutableDisposable, toDisposable } from '../common/Lifecycle'; import { MutableDisposable, toDisposable } from '../common/primitives/Lifecycle';
export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
public textarea: HTMLTextAreaElement | undefined; public textarea: HTMLTextAreaElement | undefined;
@@ -492,7 +493,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
// https://issuetracker.google.com/issues/260170397 // https://issuetracker.google.com/issues/260170397
this.textarea.setAttribute('aria-multiline', 'false'); this.textarea.setAttribute('aria-multiline', 'false');
} }
this.textarea.setAttribute('autocomplete', 'off');
this.textarea.setAttribute('autocorrect', 'off'); this.textarea.setAttribute('autocorrect', 'off');
this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false'); this.textarea.setAttribute('spellcheck', 'false');
@@ -1137,13 +1137,13 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
switch (type) { switch (type) {
case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS: case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:
const canvasWidth = this._renderService.dimensions.device.canvas.width.toFixed(0); const canvasWidth = this._renderService.dimensions.css.canvas.width.toFixed(0);
const canvasHeight = this._renderService.dimensions.device.canvas.height.toFixed(0); const canvasHeight = this._renderService.dimensions.css.canvas.height.toFixed(0);
this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`); this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
break; break;
case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS: case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:
const cellWidth = this._renderService.dimensions.device.cell.width.toFixed(0); const cellWidth = this._renderService.dimensions.css.cell.width.toFixed(0);
const cellHeight = this._renderService.dimensions.device.cell.height.toFixed(0); const cellHeight = this._renderService.dimensions.css.cell.height.toFixed(0);
this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`); this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
break; break;
} }
+2 -2
View File
@@ -5,8 +5,8 @@
* Minimal DOM helpers for xterm.js browser code. * Minimal DOM helpers for xterm.js browser code.
*/ */
import { IntervalTimer } from '../common/Async'; import { IntervalTimer } from '../common/primitives/Async';
import { IDisposable } from '../common/Lifecycle'; import { IDisposable } from '../common/primitives/Lifecycle';
export function getWindow(e: Node | UIEvent | undefined | null): Window { export function getWindow(e: Node | UIEvent | undefined | null): Window {
const candidateNode = e as Node | undefined | null; const candidateNode = e as Node | undefined | null;
+3 -3
View File
@@ -4,11 +4,11 @@
*/ */
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from './Types'; import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from './Types';
import { Disposable, dispose, toDisposable } from '../common/Lifecycle'; import { Disposable, dispose, toDisposable } from '../common/primitives/Lifecycle';
import { IDisposable } from '../common/Types'; import { IDisposable } from '../common/primitives/Types';
import { IBufferService } from '../common/services/Services'; import { IBufferService } from '../common/services/Services';
import { ILinkProviderService, IMouseCoordsService, IRenderService } from './services/Services'; import { ILinkProviderService, IMouseCoordsService, IRenderService } from './services/Services';
import { Emitter } from '../common/Event'; import { Emitter } from '../common/primitives/Event';
import { addDisposableListener } from './Dom'; import { addDisposableListener } from './Dom';
export class Linkifier extends Disposable implements ILinkifier2 { export class Linkifier extends Disposable implements ILinkifier2 {
+2 -2
View File
@@ -8,8 +8,8 @@ import { OscLinkProvider } from './OscLinkProvider';
import { ILink } from './Types'; import { ILink } from './Types';
import { createCellData, MockBufferService, MockOptionsService } from '../common/TestUtils.test'; import { createCellData, MockBufferService, MockOptionsService } from '../common/TestUtils.test';
import { IBufferService, IOscLinkService } from '../common/services/Services'; import { IBufferService, IOscLinkService } from '../common/services/Services';
import { IOscLinkData } from '../common/Types'; import { IOscLinkData } from '../common/primitives/Types';
import { IBufferLine } from '../common/buffer/Types'; import { IBufferLine } from '../common/primitives/buffer/Types';
class TestOscLinkService implements IOscLinkService { class TestOscLinkService implements IOscLinkService {
public serviceBrand: any; public serviceBrand: any;
+2 -2
View File
@@ -5,8 +5,8 @@
import { IBufferRange, ILink } from './Types'; import { IBufferRange, ILink } from './Types';
import { ILinkProvider } from './services/Services'; import { ILinkProvider } from './services/Services';
import { CellData } from '../common/buffer/CellData'; import { CellData } from '../common/primitives/buffer/CellData';
import { IBufferLine } from '../common/buffer/Types'; import { IBufferLine } from '../common/primitives/buffer/Types';
import { IBufferService, IOptionsService, IOscLinkService } from '../common/services/Services'; import { IBufferService, IOptionsService, IOscLinkService } from '../common/services/Services';
export class OscLinkProvider implements ILinkProvider { export class OscLinkProvider implements ILinkProvider {
+3 -3
View File
@@ -6,10 +6,10 @@
import { MockCompositionHelper, MockRenderer, MockViewport, TestTerminal } from './TestUtils.test'; import { MockCompositionHelper, MockRenderer, MockViewport, TestTerminal } from './TestUtils.test';
import type { IBrowser } from './Types'; import type { IBrowser } from './Types';
import { assert } from 'chai'; import { assert } from 'chai';
import { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine'; import { DEFAULT_ATTR_DATA } from '../common/primitives/buffer/BufferLine';
import { CellData } from '../common/buffer/CellData'; import { CellData } from '../common/primitives/buffer/CellData';
import { MockUnicodeService, createCellData } from '../common/TestUtils.test'; import { MockUnicodeService, createCellData } from '../common/TestUtils.test';
import { IMarker } from '../common/buffer/Types'; import { IMarker } from '../common/primitives/buffer/Types';
const INIT_COLS = 80; const INIT_COLS = 80;
const INIT_ROWS = 24; const INIT_ROWS = 24;
+10 -9
View File
@@ -7,19 +7,20 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration, I
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from './services/Services'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from './services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from './renderer/shared/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from './renderer/shared/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from './Types'; import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from './Types';
import { IAttributeData, IBuffer, IBufferLine, IBufferSet, ICellData } from '../common/buffer/Types'; import { IAttributeData, IBuffer, IBufferLine, IBufferSet, ICellData } from '../common/primitives/buffer/Types';
import { ICircularList } from '../common/CircularList'; import { ICircularList } from '../common/primitives/CircularList';
import { XtermListener, ICharset, ITerminalOptions, ColorIndex } from '../common/Types'; import { XtermListener, ICharset, ColorIndex } from '../common/primitives/Types';
import { Buffer } from '../common/buffer/Buffer'; import { ITerminalOptions } from '../common/Types';
import * as Browser from '../common/Platform'; import { Buffer } from '../common/primitives/buffer/Buffer';
import * as Browser from '../common/primitives/Platform';
import { CoreBrowserTerminal } from './CoreBrowserTerminal'; import { CoreBrowserTerminal } from './CoreBrowserTerminal';
import { IUnicodeService, IOptionsService, ICoreService, IMouseStateService } from '../common/services/Services'; import { IUnicodeService, IOptionsService, ICoreService, IMouseStateService } from '../common/services/Services';
import { IFunctionIdentifier, IParams } from '../common/parser/Types'; import { IFunctionIdentifier, IParams } from '../common/primitives/parser/Types';
import { AttributeData } from '../common/buffer/AttributeData'; import { AttributeData } from '../common/primitives/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from './selection/Types'; import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from './selection/Types';
import { css } from '../common/Color'; import { css } from '../common/primitives/Color';
import { createRenderDimensions } from './renderer/shared/RendererUtils'; import { createRenderDimensions } from './renderer/shared/RendererUtils';
import { Emitter, type IEvent } from '../common/Event'; import { Emitter, type IEvent } from '../common/primitives/Event';
export class TestTerminal extends CoreBrowserTerminal { export class TestTerminal extends CoreBrowserTerminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
+5 -4
View File
@@ -3,12 +3,13 @@
* @license MIT * @license MIT
*/ */
import { IColor, ITerminalOptions } from '../common/Types'; import { IColor } from '../common/primitives/Types';
import { CharData, IBuffer } from '../common/buffer/Types'; import { ITerminalOptions } from '../common/Types';
import { CharData, IBuffer } from '../common/primitives/buffer/Types';
import { ICoreTerminal } from '../common/CoreTerminal'; import { ICoreTerminal } from '../common/CoreTerminal';
import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm'; import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm';
import { channels, css } from '../common/Color'; import { channels, css } from '../common/primitives/Color';
import type { IEvent } from '../common/Event'; import type { IEvent } from '../common/primitives/Event';
/** /**
* A portion of the public API that are implemented identially internally and simply passed through. * A portion of the public API that are implemented identially internally and simply passed through.
+3 -3
View File
@@ -5,13 +5,13 @@
import { ICoreBrowserService, IRenderService, IThemeService } from './services/Services'; import { ICoreBrowserService, IRenderService, IThemeService } from './services/Services';
import { ViewportConstants } from './shared/Constants'; import { ViewportConstants } from './shared/Constants';
import { Disposable, toDisposable } from '../common/Lifecycle'; import { Disposable, toDisposable } from '../common/primitives/Lifecycle';
import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from '../common/services/Services'; import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from '../common/services/Services';
import { CoreMouseEventType } from '../common/Types'; import { CoreMouseEventType } from '../common/primitives/Types';
import { scheduleAtNextAnimationFrame } from './Dom'; import { scheduleAtNextAnimationFrame } from './Dom';
import { SmoothScrollableElement } from './scrollable/scrollableElement'; import { SmoothScrollableElement } from './scrollable/scrollableElement';
import type { IScrollableElementChangeOptions } from './scrollable/scrollableElementOptions'; import type { IScrollableElementChangeOptions } from './scrollable/scrollableElementOptions';
import { Emitter, EventUtils } from '../common/Event'; import { Emitter, EventUtils } from '../common/primitives/Event';
import { Scrollable, ScrollbarVisibility, type IScrollEvent } from './scrollable/scrollable'; import { Scrollable, ScrollbarVisibility, type IScrollEvent } from './scrollable/scrollable';
export class Viewport extends Disposable { export class Viewport extends Disposable {
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { ICoreBrowserService, IRenderService } from '../services/Services'; import { ICoreBrowserService, IRenderService } from '../services/Services';
import { Disposable, toDisposable } from '../../common/Lifecycle'; import { Disposable, toDisposable } from '../../common/primitives/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration } from '../../common/services/Services'; import { IBufferService, IDecorationService, IInternalDecoration } from '../../common/services/Services';
export class BufferDecorationRenderer extends Disposable { export class BufferDecorationRenderer extends Disposable {
@@ -5,7 +5,7 @@
import { ColorZoneStore, IColorZone, IColorZoneStore } from './ColorZoneStore'; import { ColorZoneStore, IColorZone, IColorZoneStore } from './ColorZoneStore';
import { ICoreBrowserService, IRenderService, IThemeService } from '../services/Services'; import { ICoreBrowserService, IRenderService, IThemeService } from '../services/Services';
import { Disposable, toDisposable } from '../../common/Lifecycle'; import { Disposable, toDisposable } from '../../common/primitives/Lifecycle';
import { IBufferService, IDecorationService, IOptionsService } from '../../common/services/Services'; import { IBufferService, IDecorationService, IOptionsService } from '../../common/services/Services';
const enum Constants { const enum Constants {
+1 -1
View File
@@ -5,7 +5,7 @@
import { IRenderService } from '../services/Services'; import { IRenderService } from '../services/Services';
import { IBufferService, ICoreService, IOptionsService } from '../../common/services/Services'; import { IBufferService, ICoreService, IOptionsService } from '../../common/services/Services';
import { C0 } from '../../common/data/EscapeSequences'; import { C0 } from '../../common/primitives/data/EscapeSequences';
interface IPosition { interface IPosition {
start: number; start: number;
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { C0 } from '../../common/data/EscapeSequences'; import { C0 } from '../../common/primitives/data/EscapeSequences';
import { IBufferService } from '../../common/services/Services'; import { IBufferService } from '../../common/services/Services';
const enum Direction { const enum Direction {
+2 -2
View File
@@ -6,14 +6,14 @@
import * as Strings from '../LocalizableStrings'; import * as Strings from '../LocalizableStrings';
import { CoreBrowserTerminal as TerminalCore } from '../CoreBrowserTerminal'; import { CoreBrowserTerminal as TerminalCore } from '../CoreBrowserTerminal';
import { IBufferRange, ITerminal } from '../Types'; import { IBufferRange, ITerminal } from '../Types';
import { Disposable } from '../../common/Lifecycle'; import { Disposable } from '../../common/primitives/Lifecycle';
import { ITerminalOptions } from '../../common/Types'; import { ITerminalOptions } from '../../common/Types';
import { AddonManager } from '../../common/public/AddonManager'; import { AddonManager } from '../../common/public/AddonManager';
import { BufferNamespaceApi } from '../../common/public/BufferNamespaceApi'; import { BufferNamespaceApi } from '../../common/public/BufferNamespaceApi';
import { ParserApi } from '../../common/public/ParserApi'; import { ParserApi } from '../../common/public/ParserApi';
import { UnicodeApi } from '../../common/public/UnicodeApi'; import { UnicodeApi } from '../../common/public/UnicodeApi';
import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, IRenderDimensions, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm'; import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, IRenderDimensions, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
import type { IEvent } from '../../common/Event'; import type { IEvent } from '../../common/primitives/Event';
/** /**
* The set of options that only have an effect when set in the Terminal constructor. * The set of options that only have an effect when set in the Terminal constructor.
+3 -3
View File
@@ -12,10 +12,10 @@ import { TextBlinkStateManager } from '../shared/TextBlinkStateManager';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderModel } from '../shared/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderModel } from '../shared/Types';
import { ICharSizeService, ICoreBrowserService, IThemeService } from '../../services/Services'; import { ICharSizeService, ICoreBrowserService, IThemeService } from '../../services/Services';
import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from '../../Types'; import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from '../../Types';
import { color } from '../../../common/Color'; import { color } from '../../../common/primitives/Color';
import { Disposable, toDisposable } from '../../../common/Lifecycle'; import { Disposable, toDisposable } from '../../../common/primitives/Lifecycle';
import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from '../../../common/services/Services'; import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from '../../../common/services/Services';
import { Emitter } from '../../../common/Event'; import { Emitter } from '../../../common/primitives/Event';
import { addDisposableListener } from '../../Dom'; import { addDisposableListener } from '../../Dom';
@@ -6,14 +6,16 @@
import jsdom = require('jsdom'); import jsdom = require('jsdom');
import { assert } from 'chai'; import { assert } from 'chai';
import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DomRendererRowFactory } from './DomRendererRowFactory';
import { DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from '../../../common/buffer/Constants'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from '../../../common/primitives/buffer/Constants';
import { BufferLine, DEFAULT_ATTR_DATA } from '../../../common/buffer/BufferLine'; import { BufferLine, DEFAULT_ATTR_DATA } from '../../../common/primitives/buffer/BufferLine';
import { IBufferLine } from '../../../common/buffer/Types'; import { BufferLineStringCache } from '../../../common/primitives/buffer/BufferLineStringCache';
import { CellData } from '../../../common/buffer/CellData'; import { IBufferLine } from '../../../common/primitives/buffer/Types';
import { CellData } from '../../../common/primitives/buffer/CellData';
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from '../../../common/TestUtils.test'; import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from '../../../common/TestUtils.test';
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from '../../TestUtils.test'; import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from '../../TestUtils.test';
import { TestWidthCache } from './WidthCache.test'; import { TestWidthCache } from './WidthCache.test';
const TEST_STRING_CACHE = new BufferLineStringCache();
describe('DomRendererRowFactory', () => { describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM; let dom: jsdom.JSDOM;
@@ -515,7 +517,7 @@ describe('DomRendererRowFactory', () => {
} }
function createEmptyLineData(cols: number): IBufferLine { function createEmptyLineData(cols: number): IBufferLine {
const lineData = new BufferLine(cols); const lineData = new BufferLine(TEST_STRING_CACHE, cols);
for (let i = 0; i < cols; i++) { for (let i = 0; i < cols; i++) {
lineData.setCell(i, NULL_CELL_DATA); lineData.setCell(i, NULL_CELL_DATA);
} }
@@ -3,17 +3,17 @@
* @license MIT * @license MIT
*/ */
import { IColor } from '../../../common/Types'; import { IColor } from '../../../common/primitives/Types';
import { IBufferLine, ICellData } from '../../../common/buffer/Types'; import { IBufferLine, ICellData } from '../../../common/primitives/buffer/Types';
import { INVERTED_DEFAULT_COLOR } from '../shared/Constants'; import { INVERTED_DEFAULT_COLOR } from '../shared/Constants';
import { WHITESPACE_CELL_CHAR, Attributes } from '../../../common/buffer/Constants'; import { WHITESPACE_CELL_CHAR, Attributes } from '../../../common/primitives/buffer/Constants';
import { CellData } from '../../../common/buffer/CellData'; import { CellData } from '../../../common/primitives/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from '../../../common/services/Services'; import { ICoreService, IDecorationService, IOptionsService } from '../../../common/services/Services';
import { channels, color } from '../../../common/Color'; import { channels, color } from '../../../common/primitives/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from '../../services/Services'; import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from '../../services/Services';
import { JoinedCellData } from '../../services/CharacterJoinerService'; import { JoinedCellData } from '../../services/CharacterJoinerService';
import { treatGlyphAsBackgroundColor } from '../shared/RendererUtils'; import { treatGlyphAsBackgroundColor } from '../shared/RendererUtils';
import { AttributeData } from '../../../common/buffer/AttributeData'; import { AttributeData } from '../../../common/primitives/buffer/AttributeData';
import { WidthCache } from './WidthCache'; import { WidthCache } from './WidthCache';
import { IColorContrastCache } from '../../Types'; import { IColorContrastCache } from '../../Types';
+1 -1
View File
@@ -4,7 +4,7 @@
*/ */
import { throwIfFalsy } from '../shared/RendererUtils'; import { throwIfFalsy } from '../shared/RendererUtils';
import { IDisposable } from '../../../common/Types'; import { IDisposable } from '../../../common/primitives/Types';
import { FontWeight } from '../../../common/services/Services'; import { FontWeight } from '../../../common/services/Services';
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { TextBlinkStateManager } from './TextBlinkStateManager'; import { TextBlinkStateManager } from './TextBlinkStateManager';
import { MockOptionsService } from '../../../common/TestUtils.test'; import { MockOptionsService } from '../../../common/TestUtils.test';
import type { ICoreBrowserService } from '../../services/Services'; import type { ICoreBrowserService } from '../../services/Services';
import { Emitter } from '../../../common/Event'; import { Emitter } from '../../../common/primitives/Event';
class FakeWindow { class FakeWindow {
public nextId = 1; public nextId = 1;
@@ -4,7 +4,7 @@
*/ */
import { ICoreBrowserService } from '../../services/Services'; import { ICoreBrowserService } from '../../services/Services';
import { Disposable, toDisposable } from '../../../common/Lifecycle'; import { Disposable, toDisposable } from '../../../common/primitives/Lifecycle';
import { IOptionsService } from '../../../common/services/Services'; import { IOptionsService } from '../../../common/services/Services';
export class TextBlinkStateManager extends Disposable { export class TextBlinkStateManager extends Disposable {
+2 -2
View File
@@ -5,8 +5,8 @@
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { ITerminal } from '../../Types'; import { ITerminal } from '../../Types';
import { IDisposable } from '../../../common/Types'; import { IDisposable } from '../../../common/primitives/Types';
import type { IEvent } from '../../../common/Event'; import type { IEvent } from '../../../common/primitives/Event';
export interface IDimensions { export interface IDimensions {
width: number; width: number;
+1 -1
View File
@@ -11,7 +11,7 @@ import { ScrollbarArrow, IScrollbarArrowOptions } from './scrollbarArrow';
import { ScrollbarState } from './scrollbarState'; import { ScrollbarState } from './scrollbarState';
import { ScrollbarVisibilityController } from './scrollbarVisibilityController'; import { ScrollbarVisibilityController } from './scrollbarVisibilityController';
import { Widget } from './widget'; import { Widget } from './widget';
import * as platform from '../../common/Platform'; import * as platform from '../../common/primitives/Platform';
import { INewScrollPosition, Scrollable, ScrollbarVisibility } from './scrollable'; import { INewScrollPosition, Scrollable, ScrollbarVisibility } from './scrollable';
/** /**
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as dom from '../Dom'; import * as dom from '../Dom';
import { DisposableStore, IDisposable, toDisposable } from '../../common/Lifecycle'; import { DisposableStore, IDisposable, toDisposable } from '../../common/primitives/Lifecycle';
type PointerMoveCallback = (event: PointerEvent) => void; type PointerMoveCallback = (event: PointerEvent) => void;
type OnStopCallback = () => void; type OnStopCallback = () => void;
+1 -1
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as platform from '../../common/Platform'; import * as platform from '../../common/primitives/Platform';
interface IWindowChainElement { interface IWindowChainElement {
readonly window: WeakRef<Window>; readonly window: WeakRef<Window>;

Some files were not shown because too many files have changed in this diff Show More