Compare commits

...
Author SHA1 Message Date
Cursor AgentandDaniel Imms b6bf7fa4ab docs(browser): fix layer rules — renderer/dom is integration, not platform
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-30 16:11:55 +00:00
Cursor AgentandDaniel Imms a53adde059 docs(browser): propose layered TS project split for browser/
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-30 15:44:57 +00:00
Daniel Imms 43e8365f0e Merge pull request #5960 from xtermjs/cursor/move-fixtures-1191
Move fixtures to test/fixtures
2026-05-28 07:06:21 -07:00
Cursor AgentandDaniel Imms c7a94fc0bc Move fixtures to test/fixtures and update references
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-28 14:01:12 +00:00
Daniel Imms 4bb9c06b5f Merge pull request #5953 from xtermjs/cursor/alt-click-for-mouse-events-5b17
Add altClickForMouseEvents option
2026-05-28 06:35:56 -07:00
Daniel Imms 00e54532b3 Merge pull request #5959 from xtermjs/cursor/setup-dev-environment-dc0c
Add Cursor Cloud specific instructions to AGENTS.md
2026-05-27 22:13:27 -07:00
Cursor AgentandDaniel Imms e96133603b Add Cursor Cloud specific instructions to AGENTS.md
Add development environment guidance for cloud agents including:
- Demo server startup notes
- Build-before-test requirements
- Note about no external services needed
- tsgo compiler note
- Lint optimization tip

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-28 05:10:34 +00:00
Daniel Imms b2003861ec Merge pull request #5859 from xtermjs/iip_enhancements
IIP enhancements
2026-05-27 20:36:13 -07:00
Daniel Imms 38f77fd4ca Merge branch 'master' into iip_enhancements 2026-05-27 20:16:36 -07:00
Daniel Imms d3352cf1d5 Merge pull request #5873 from xtermjs/package_upgrades
package upgrades & linter fixes
2026-05-27 20:16:08 -07:00
Daniel Imms 0c4ec34df8 Remove unnecessary @types/lru-cache 2026-05-27 20:11:20 -07:00
Daniel Imms 743852b60c Remove unneeded comment 2026-05-27 19:58:52 -07:00
Daniel Imms 2069176528 Fix lint 2026-05-27 19:58:21 -07:00
Daniel Imms acd2472961 Merge remote-tracking branch 'upstream/master' into package_upgrades 2026-05-27 19:23:43 -07:00
Daniel Imms c2ecfe7446 Merge pull request #5895 from xtermjs/dependabot/npm_and_yarn/multi-f792d6d6d9
Bump qs and express
2026-05-27 18:26:29 -07:00
Daniel Imms 5656443163 Merge pull request #5957 from xtermjs/cursor/fix-progress-webkit-flake-5577
fix(addon-progress): stabilize flaky WebKit integration test harness
2026-05-27 17:32:25 -07:00
Cursor AgentandDaniel Imms 38174907f3 fix(addon-progress): stabilize WebKit integration test harness
Remove unnecessary unawaited setViewportSize from ProgressAddon tests,
which was the line reported in flaky CI failures. Use serial mode and
scoped hooks per Playwright shared-page guidance, and guard afterAll
teardown when setup fails.

Fixes #5956

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-28 00:24:06 +00:00
Daniel ImmsandCursor 1b95efa8c8 Add explicit void return type to alt cursor event listener.
Fixes explicit-function-return-type lint in AltMouseCursorController.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 16:54:23 -07:00
Daniel ImmsandCursor ce1f5f9608 Add MouseEventCssClasses const enum for enable-mouse-events class.
Centralize the CSS class name in MouseService.ts and use it across mouse mode handling and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 16:23:17 -07:00
Daniel ImmsandCursor bf8af4a871 Toggle enable-mouse-events cursor while alt is held for mouseEventsRequireAlt.
Extract AltMouseCursorController to bind DOM listeners only when the option and mouse protocol are active.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 16:20:45 -07:00
Daniel ImmsandCursor f8e9a81d0e Fix mouseEventsRequireAlt by stripping alt from PTY mouse reports.
Alt is only used locally to gate passthrough; applications like tmux ignore alt-modified SGR sequences.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 16:11:35 -07:00
Cursor AgentandDaniel Imms 7711d780c2 Handle mouseEventsRequireAlt option changes in mouse mode
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:59:23 +00:00
dependabot[bot] e89fbf2979 Bump qs and express
Bumps [qs](https://github.com/ljharb/qs) to 6.15.2 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `qs` from 6.14.2 to 6.15.2
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.2...v6.15.2)

Updates `express` from 4.22.1 to 4.22.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/v4.22.1...v4.22.2)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.2
  dependency-type: direct:development
- dependency-name: qs
  dependency-version: 6.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 21:53:01 +00:00
Cursor AgentandDaniel Imms f8cef535c7 Rename altClickForMouseEvents to mouseEventsRequireAlt
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:41:44 +00:00
Cursor AgentandDaniel Imms 5ad56446b8 Explicitly enable selection when altClickForMouseEvents is on at attach
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:20:17 +00:00
Cursor AgentandDaniel Imms fbb2a9710f Add altClickForMouseEvents option for issue #5952
When enabled during mouse events mode, click/drag/move events require the alt
key while wheel events are unaffected. Selection stays enabled by default
and takes precedence over macOptionClickForcesSelection.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 20:13:31 +00:00
Jörg Breitbart 5a6427e867 Merge branch 'master' into package_upgrades 2026-05-21 13:45:03 +02:00
Jörg Breitbart 8519ac6d99 Merge branch 'master' into package_upgrades 2026-05-21 13:34:12 +02:00
jerch ae611694de Merge branch 'master' into iip_enhancements 2026-05-21 12:47:09 +02:00
jerch 772df0cb98 Merge branch 'master' into iip_enhancements 2026-05-11 17:03:53 +02:00
jerch aa93189cc5 Merge branch 'master' into package_upgrades 2026-05-11 16:57:49 +02:00
Jörg Breitbart f82a8029d2 fix README 2026-05-10 21:27:52 +02:00
Jörg Breitbart a2f321c887 add multipart demo 2026-05-10 21:08:22 +02:00
Jörg Breitbart eb58cefa8e add integration tests for filepart 2026-05-10 20:34:43 +02:00
Jörg Breitbart d92ca87cbe add qoi metrics test 2026-05-10 19:32:47 +02:00
jerch 224167bbbe Merge branch 'master' into iip_enhancements 2026-05-09 22:59:22 +02:00
Jörg Breitbart 740fa44ae4 make webfont loading tests more resilient 2026-05-09 18:13:16 +02:00
Jörg Breitbart 636baa8d79 make webfont loading tests more resilient 2026-05-09 18:06:59 +02:00
Jörg Breitbart 44a27dc958 make webfont loading tests more resilient 2026-05-09 17:59:33 +02:00
Jörg Breitbart dc1ae4514b package upgrades & linter fixes 2026-05-09 17:42:49 +02:00
Jörg Breitbart 494cf1f2e1 make linter happy 2026-04-30 16:54:54 +02:00
Jörg Breitbart 645865efec fix cursor placement in IIP 2026-04-30 16:46:22 +02:00
Jörg Breitbart 83e3657a82 fix _mouseCoordsService access in demo 2026-04-30 15:21:41 +02:00
Jörg Breitbart 53554a05f7 use _coreBrowserService.dpr 2026-04-30 10:44:38 +02:00
Jörg Breitbart 51c8d8abf8 implement ReportCellSize 2026-04-30 10:36:41 +02:00
Jörg Breitbart 05ea5ac324 implement ReportCellSize 2026-04-30 10:33:34 +02:00
Jörg Breitbart 9e27c7debb filepart handling in IIP 2026-04-29 21:36:45 +02:00
221 changed files with 3671 additions and 2511 deletions
+12
View File
@@ -98,3 +98,15 @@ const cell = line?.getCell(0);
## Writing unit tests ## Writing unit tests
- Unit tests live alongside the source code file of the thing it's testing with a .test.ts suffix. - Unit tests live alongside the source code file of the thing it's testing with a .test.ts suffix.
## Cursor Cloud specific instructions
**Demo server**: Start with `npm start` (port 3000). The demo server uses node-pty to spawn real shell sessions over WebSocket. Integration tests auto-start it via Playwright's `webServer` config, so you don't need to start it manually for `npm run test-integration`.
**Build before testing**: Always run `npm run build && npm run esbuild` before `npm run test-unit`. Integration tests also need `npm run esbuild-demo-client` and `npm run esbuild-demo-server`. The update script handles this automatically on session start.
**No external services**: This project has zero external dependencies (no databases, Docker, or APIs). Everything runs locally with Node.js.
**TypeScript compiler**: The project uses `tsgo` (native TypeScript compiler preview) rather than standard `tsc`. It's installed via the `@typescript/native-preview` package.
**Lint only changed files**: Prefer `npm run lint-changes` over `npm run lint` when iterating on code changes — it's significantly faster.
+20 -31
View File
@@ -1,6 +1,7 @@
## @xterm/addon-image ## @xterm/addon-image
Inline image output in xterm.js. Supports SIXEL and iTerm's inline image protocol (IIP). Inline image output in xterm.js. Supports SIXEL, iTerm's inline image protocol (IIP)
and partially Kitty's terminal graphics prtocol (TGP).
![](fixture/example.png) ![](fixture/example.png)
@@ -43,9 +44,6 @@ terminal.loadAddon(imageAddon);
### General Notes ### General Notes
- *IMPORTANT:* The worker approach as done in previous releases got removed.
The addon contructor no longer expects a worker path as first argument.
- By default the addon will activate these `windowOptions` reports on the terminal: - By default the addon will activate these `windowOptions` reports on the terminal:
- getWinSizePixels (CSI 14 t) - getWinSizePixels (CSI 14 t)
- getCellSizePixels (CSI 16 t) - getCellSizePixels (CSI 16 t)
@@ -73,7 +71,7 @@ terminal.loadAddon(imageAddon);
- **Cursor Positioning** - **Cursor Positioning**
If scrolling is set, the cursor will be placed at the first image column of the last image row (VT340 mode). If scrolling is set, the cursor will be placed at the first image column of the last image row (VT340 mode).
Other cursor positioning modes as used by xterm or mintty are not supported. Other cursor positioning modes as used by xterm or mintty are not supported for sixel.
- **SIXEL Palette Handling** - **SIXEL Palette Handling**
By default the addon limits the palette size to 256 registers (as demanded by the DEC specification). By default the addon limits the palette size to 256 registers (as demanded by the DEC specification).
@@ -100,17 +98,24 @@ 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 and GIF - 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.
- Filename gets parsed but not used. - Filename gets parsed but not used.
- Strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding, no separator bytes allowed). - Strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding, no separator bytes allowed).
- Payload size may not exceed CEIL(sizeParameter * 4 / 3).
- Image scaling beyond terminal viewport size is allowed (e.g. `width=200%`). - Image scaling beyond terminal viewport size is allowed (e.g. `width=200%`).
- Image pixel size is restricted by `pixelLimit` (pre- and post resizing). - Image pixel size is restricted by `pixelLimit` (pre- and post resizing).
- Size parameter is restricted by `iipSizeLimit`. - Playload size is restricted by `iipSizeLimit`.
- Cursor positioning behaves the same as for sixel (see above). - size parameter is not mandatory anymore.
- Cursor gets positioned at the next cell of the last image line.
- ReportCellSize sequence is supported.
- Hi-res display support is currently limited (image gets blurred).
- **Kitty Graphics Support (TGP)**
Set by default, change it with `{kittySupport: true}`.
Note that the kitty graphics support is still WIP.
### Storage and Drawing Settings ### Storage and Drawing Settings
@@ -127,6 +132,7 @@ The addon exposes two properties to interact with the storage limits at runtime:
By default the addon will show a placeholder pattern for evicted images that are still part By default the addon will show a placeholder pattern for evicted images that are still part
of the terminal (e.g. in the scrollback). The pattern can be deactivated by toggling `showPlaceholder`. of the terminal (e.g. in the scrollback). The pattern can be deactivated by toggling `showPlaceholder`.
### Image Data Retrieval ### Image Data Retrieval
The addon provides the following API endpoints to retrieve raw image data as canvas: The addon provides the following API endpoints to retrieve raw image data as canvas:
@@ -140,6 +146,7 @@ The addon provides the following API endpoints to retrieve raw image data as can
The buffer position is the 0-based absolute index (including scrollback at top). The buffer position is the 0-based absolute index (including scrollback at top).
Note that the canvas gets created and data copied over for every call, thus it is not suitable for performance critical actions. Note that the canvas gets created and data copied over for every call, thus it is not suitable for performance critical actions.
### Memory Usage ### Memory Usage
The addon does most image processing in Javascript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, lets look at the data flow and processing steps: The addon does most image processing in Javascript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, lets look at the data flow and processing steps:
@@ -180,8 +187,8 @@ _How can I adjust the memory usage?_
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage is only temporarily peaking, a rather high value should work even with multiple terminals in one page. A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage is only temporarily peaking, a rather high value should work even with multiple terminals in one page.
- `storageLimit` - `storageLimit`
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for memory peaking for decoding. A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for memory peaking for decoding.
- `sixelSizeLimit` - `sixelSizeLimit | iipSizeLimit | kittySizeLimit`
A constructor setting. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken SIXEL sequences at an earlier phase, thus may stop the decoder from entering its memory intensive task for potentially invalid data. Constructor settings. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken image sequences at an earlier phase, thus may stop the invoked decoders from entering memory intensive tasks for potentially invalid data.
### Terminal Interaction ### Terminal Interaction
@@ -211,23 +218,5 @@ _How can I adjust the memory usage?_
### Status ### Status
- Sixel support and image handling in xterm.js is considered beta quality. - Sixel and IIP support and image handling in xterm.js is considered beta quality.
- IIP support is in alpha stage. Please file a bug for any awkwardities. - Kitty support is in alpha stage. Please file a bug for any awkwardities or missing features.
### Changelog
- 0.5.0 integrate with xtermjs base repo (at v0.4.3)
- 0.4.3 defer canvas creation
- 0.4.2 fix image canvas resize
- 0.4.1 compat release for xterm.js 5.2.0
- 0.4.0 IIP support
- 0.3.1 compat release for xterm.js 5.1.0
- 0.3.0 important change: worker removed from addon
- 0.2.0 compat release for xterm.js 5.0.0
- 0.1.3 bugfix: avoid striping
- 0.1.2 bugfix: reset clear flag
- 0.1.1 bugfixes:
- clear sticky image tiles on render
- create folder from bootstrap.sh
- fix peer dependency in package.json
Binary file not shown.
+70 -16
View File
@@ -8,7 +8,7 @@ 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 } from './IIPHeaderParser'; import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics'; import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
// 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
@@ -23,6 +23,7 @@ const enum DecoderConst {
// default IIP header values // default IIP header values
const DEFAULT_HEADER: IHeaderFields = { const DEFAULT_HEADER: IHeaderFields = {
type: SequenceType.INVALID,
name: 'Unnamed file', name: 'Unnamed file',
size: 0, size: 0,
width: 'auto', width: 'auto',
@@ -39,6 +40,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private _dec: Base64Decoder; private _dec: Base64Decoder;
private _qoiDec: QoiDecoder; private _qoiDec: QoiDecoder;
private _metrics = UNSUPPORTED_TYPE; private _metrics = UNSUPPORTED_TYPE;
private _isMultipart = false;
private _abortMulti = false;
constructor( constructor(
private readonly _opts: IImageAddonOptions, private readonly _opts: IImageAddonOptions,
@@ -52,11 +55,14 @@ export class IIPHandler implements IOscHandler, IResetHandler {
this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA); this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
} }
public reset(): void {} public reset(): void {
this._hp.reset();
this._dec.release();
this._qoiDec.release();
}
public start(): void { public start(): void {
this._aborted = false; this._aborted = false;
this._header = DEFAULT_HEADER;
this._metrics = UNSUPPORTED_TYPE; this._metrics = UNSUPPORTED_TYPE;
this._hp.reset(); this._hp.reset();
} }
@@ -76,15 +82,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return; return;
} }
if (dataPos > 0) { if (dataPos > 0) {
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields); const seqType = this._hp.fields.type;
if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) { if (seqType === SequenceType.FILE) {
if (this._isMultipart) {
this._isMultipart = false;
this._abortMulti = false;
this._dec.release();
}
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
if (!this._header.inline) {
this._aborted = true;
return;
}
this._dec.init();
} else if (this._abortMulti) {
this._aborted = true; this._aborted = true;
return; return;
} }
this._dec.init();
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) { if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
this._dec.release(); this._dec.release();
this._aborted = true; this._aborted = true;
if (this._isMultipart) this._abortMulti = true;
} }
} }
} }
@@ -93,22 +111,58 @@ export class IIPHandler implements IOscHandler, IResetHandler {
public end(success: boolean): boolean | Promise<boolean> { public end(success: boolean): boolean | Promise<boolean> {
if (this._aborted) return true; if (this._aborted) return true;
if (this._hp.state !== HeaderState.END) {
if (this._hp.end()) return true;
}
const seqType = this._hp.fields.type;
if (seqType === SequenceType.FILEPART) return true;
if (seqType === SequenceType.REPORTCELLSIZE) {
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
let width = CELL_SIZE_DEFAULT.width;
let height = CELL_SIZE_DEFAULT.height;
if (this._renderer.dimensions) {
width = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
height = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
}
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
const report = `\x1b]1337;ReportCellSize=${height.toFixed(3)};${width.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
this._coreTerminal.input(report, false);
return true;
}
if (seqType === SequenceType.MULTIPARTFILE) {
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
this._isMultipart = true;
this._abortMulti = false;
this._dec.release();
this._dec.init();
return true;
}
if (seqType === SequenceType.FILEEND) {
if (!this._isMultipart) return true;
this._isMultipart = false;
if (this._abortMulti || this._header.type !== SequenceType.MULTIPARTFILE) return true;
}
// fallthrough for SequenceType.FILE & SequenceType.FILEEND
let w = 0; let w = 0;
let h = 0; let h = 0;
// early exit condition chain // early exit condition chain
let cond: number | boolean = true; let cond: number | boolean;
if (cond = success) { if (cond = success) {
if (cond = !this._dec.end()) { if (cond = !this._dec.end()) {
if (cond = this._dec.data8.length === this._header.size) { this._metrics = imageType(this._dec.data8);
this._metrics = imageType(this._dec.data8); if (cond = this._metrics.mime !== 'unsupported') {
if (cond = this._metrics.mime !== 'unsupported') { w = this._metrics.width;
w = this._metrics.width; h = this._metrics.height;
h = this._metrics.height; if (cond = w && h && w * h < this._opts.pixelLimit) {
if (cond = w && h && w * h < this._opts.pixelLimit) { [w, h] = this._resize(w, h).map(Math.floor);
[w, h] = this._resize(w, h).map(Math.floor); cond = w && h && w * h < this._opts.pixelLimit;
cond = w && h && w * h < this._opts.pixelLimit;
}
} }
} }
} }
+129 -7
View File
@@ -4,19 +4,20 @@
*/ */
import { assert } from 'chai'; import { assert } from 'chai';
import { HeaderParser, HeaderState, IHeaderFields } from './IIPHeaderParser'; import { HeaderParser, HeaderState, IHeaderFields, SequenceType } from './IIPHeaderParser';
const CASES: [string, IHeaderFields][] = [ const CASES: [string, IHeaderFields][] = [
['File=size=123456;name=dGVzdA==:', {name: 'test', size: 123456}], ['File=size=123456;name=dGVzdA==:', {type: SequenceType.FILE, name: 'test', size: 123456}],
['File=size=123456;name=dGVzdA:', {name: 'test', size: 123456}], ['File=size=123456;name=dGVzdA:', {type: SequenceType.FILE, name: 'test', size: 123456}],
// utf-8 encoding in name // utf-8 encoding in name
['File=size=123456;name=w7xtbMOkdXTDnw==:', {name: 'ümläutß', size: 123456}], ['File=size=123456;name=w7xtbMOkdXTDnw==:', {type: SequenceType.FILE, name: 'ümläutß', size: 123456}],
['File=size=123456;name=w7xtbMOkdXTDnw:', {name: 'ümläutß', size: 123456}], ['File=size=123456;name=w7xtbMOkdXTDnw:', {type: SequenceType.FILE, name: 'ümläutß', size: 123456}],
// full header spec // full header spec
[ [
'File=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:', 'File=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
{ {
type: SequenceType.FILE,
inline: 1, inline: 1,
width: '10px', width: '10px',
height: '20%', height: '20%',
@@ -28,6 +29,7 @@ const CASES: [string, IHeaderFields][] = [
[ [
'File=inline=1;width=auto;height=20;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:', 'File=inline=1;width=auto;height=20;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
{ {
type: SequenceType.FILE,
inline: 1, inline: 1,
width: 'auto', width: 'auto',
height: '20', height: '20',
@@ -101,7 +103,7 @@ describe('IIPHeaderParser', () => {
let res = hp.parse(inp, 0, inp.length); let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length); assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END); assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, {name: 'test', size: 0}); assert.deepEqual(hp.fields, {type: SequenceType.FILE, name: 'test', size: 0});
hp.reset(); hp.reset();
inp = fromBs(CASES[0][0]); inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length); res = hp.parse(inp, 0, inp.length);
@@ -115,7 +117,7 @@ describe('IIPHeaderParser', () => {
let res = hp.parse(inp, 0, inp.length); let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length); assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END); assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, {name: '', size: 123456}); assert.deepEqual(hp.fields, {type: SequenceType.FILE, name: '', size: 123456});
hp.reset(); hp.reset();
inp = fromBs(CASES[0][0]); inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length); res = hp.parse(inp, 0, inp.length);
@@ -135,4 +137,124 @@ describe('IIPHeaderParser', () => {
assert.strictEqual(hp.state, HeaderState.END); assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, CASES[0][1]); assert.deepEqual(hp.fields, CASES[0][1]);
}); });
it('FilePart sequence', () => {
const hp = new HeaderParser();
const inp = fromBs('FilePart=w7xtbMOkdXTDnw');
const res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, 9);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields.type, SequenceType.FILEPART);
});
it('FilePart sequence - bytewise', () => {
const hp = new HeaderParser();
const inp = fromBs('FilePart=w7xtbMOkdXTDnw');
let res = -2;
let pos = 0;
while (res === -2 && pos < inp.length) {
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
}
assert.strictEqual(res, 1);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields.type, SequenceType.FILEPART);
});
it('MultipartFile sequence', () => {
const hp = new HeaderParser();
const inp = fromBs('MultipartFile=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw');
let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, -2);
assert.strictEqual(hp.state, HeaderState.VALUE);
res = hp.end();
assert.strictEqual(res, 0);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(
hp.fields,
{
type: SequenceType.MULTIPARTFILE,
inline: 1,
width: '10px',
height: '20%',
preserveAspectRatio: 1,
size: 123456,
name: 'ümläutß'
}
);
});
it('MultipartFile sequence - bytewise', () => {
const hp = new HeaderParser();
const inp = fromBs('MultipartFile=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw');
let res = -2;
let pos = 0;
while (res === -2 && pos < inp.length) {
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
}
assert.strictEqual(res, -2);
assert.strictEqual(hp.state, HeaderState.VALUE);
res = hp.end();
assert.strictEqual(res, 0);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(
hp.fields,
{
type: SequenceType.MULTIPARTFILE,
inline: 1,
width: '10px',
height: '20%',
preserveAspectRatio: 1,
size: 123456,
name: 'ümläutß'
}
);
});
it('FileEnd sequence', () => {
const hp = new HeaderParser();
const inp = fromBs('FileEnd');
let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, -2);
assert.strictEqual(hp.state, HeaderState.START);
res = hp.end();
assert.strictEqual(res, 0);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields.type, SequenceType.FILEEND);
});
it('FileEnd sequence - bytewise', () => {
const hp = new HeaderParser();
const inp = fromBs('FileEnd');
let res = -2;
let pos = 0;
while (res === -2 && pos < inp.length) {
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
}
assert.strictEqual(res, -2);
assert.strictEqual(hp.state, HeaderState.START);
res = hp.end();
assert.strictEqual(res, 0);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields.type, SequenceType.FILEEND);
});
it('ReportCellSize sequence', () => {
const hp = new HeaderParser();
const inp = fromBs('ReportCellSize');
let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, -2);
assert.strictEqual(hp.state, HeaderState.START);
res = hp.end();
assert.strictEqual(res, 0);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, { type: SequenceType.REPORTCELLSIZE });
});
it('ReportCellSize sequence - bytewise', () => {
const hp = new HeaderParser();
const inp = fromBs('ReportCellSize');
let res = -2;
let pos = 0;
while (res === -2 && pos < inp.length) {
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
}
assert.strictEqual(res, -2);
assert.strictEqual(hp.state, HeaderState.START);
res = hp.end();
assert.strictEqual(res, 0);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, { type: SequenceType.REPORTCELLSIZE });
});
}); });
+86 -11
View File
@@ -6,9 +6,27 @@
// eslint-disable-next-line // eslint-disable-next-line
declare const Buffer: any; declare const Buffer: any;
export const enum HeaderState {
START = 0,
ABORT = 1,
KEY = 2,
VALUE = 3,
END = 4
}
export const enum SequenceType {
INVALID = 0,
FILE = 1,
MULTIPARTFILE = 2,
FILEPART = 3,
FILEEND = 4,
REPORTCELLSIZE = 5
}
export interface IHeaderFields { export interface IHeaderFields {
[key: string]: number | string | Uint32Array | null | undefined; [key: string]: number | string | Uint32Array | null | undefined;
// sequence type
type: SequenceType;
// base-64 encoded filename. Defaults to "Unnamed file". // base-64 encoded filename. Defaults to "Unnamed file".
name: string; name: string;
// File size in bytes. The file transfer will be canceled if this size is exceeded. // File size in bytes. The file transfer will be canceled if this size is exceeded.
@@ -29,14 +47,6 @@ export interface IHeaderFields {
inline?: number; inline?: number;
} }
export const enum HeaderState {
START = 0,
ABORT = 1,
KEY = 2,
VALUE = 3,
END = 4
}
// field value decoders // field value decoders
// ASCII bytes to string // ASCII bytes to string
@@ -92,7 +102,19 @@ const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
}; };
// sequence type markers
// File
const FILE_MARKER = [70, 105, 108, 101]; const FILE_MARKER = [70, 105, 108, 101];
// MultipartFile
const MULTIPARTFILE_MARKER = [77, 117, 108, 116, 105, 112, 97, 114, 116, 70, 105, 108, 101];
// FilePart
const FILEPART_MARKER = [70, 105, 108, 101, 80, 97, 114, 116];
// FileEnd
const FILEEND_MARKER = [70, 105, 108, 101, 69, 110, 100];
// ReportCellSize
const REPORTCELLSIZE_MARKER = [82, 101, 112, 111, 114, 116, 67, 101, 108, 108, 83, 105, 122, 101];
// max allowed chars for sequence header
const MAX_FIELDCHARS = 1024; const MAX_FIELDCHARS = 1024;
@@ -111,12 +133,43 @@ export class HeaderParser {
this._key = ''; this._key = '';
} }
public end(): number {
if (this.state === HeaderState.START) {
if (this._position === FILEEND_MARKER.length) {
for (let k = 0; k < FILEEND_MARKER.length; ++k) {
if (this._buffer[k] !== FILEEND_MARKER[k]) return this._a();
}
this.fields['type'] = SequenceType.FILEEND;
this.state = HeaderState.END;
return 0;
}
if (this._position === REPORTCELLSIZE_MARKER.length) {
for (let k = 0; k < REPORTCELLSIZE_MARKER.length; ++k) {
if (this._buffer[k] !== REPORTCELLSIZE_MARKER[k]) return this._a();
}
this.fields['type'] = SequenceType.REPORTCELLSIZE;
this.state = HeaderState.END;
return 0;
}
return this._a();
}
if (this.state === HeaderState.END) return 0;
if (this.state === HeaderState.VALUE
&& this.fields.type === SequenceType.MULTIPARTFILE
) {
if (!this._storeValue(this._position)) return this._a();
this.state = HeaderState.END;
return 0;
}
return this._a();
}
public parse(data: Uint32Array, start: number, end: number): number { public parse(data: Uint32Array, start: number, end: number): number {
let state = this.state; let state = this.state;
let pos = this._position; let pos = this._position;
const buffer = this._buffer; const buffer = this._buffer;
if (state === HeaderState.ABORT || state === HeaderState.END) return -1; if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
if (state === HeaderState.START && pos > 6) return -1; if (state === HeaderState.START && pos > 14) return -1;
for (let i = start; i < end; ++i) { for (let i = start; i < end; ++i) {
const c = data[i]; const c = data[i];
switch (c) { switch (c) {
@@ -127,8 +180,29 @@ export class HeaderParser {
break; break;
case 61: // = case 61: // =
if (state === HeaderState.START) { if (state === HeaderState.START) {
for (let k = 0; k < FILE_MARKER.length; ++k) { if (buffer[0] === 70) {
if (buffer[k] !== FILE_MARKER[k]) return this._a(); // 'File' or 'FilePart'
let k = 0;
for (; k < FILE_MARKER.length; ++k) {
if (buffer[k] !== FILE_MARKER[k]) return this._a();
}
this.fields['type'] = SequenceType.FILE;
if (pos === FILEPART_MARKER.length) {
for (; k < FILEPART_MARKER.length; ++k) {
if (buffer[k] !== FILEPART_MARKER[k]) return this._a();
}
this.fields['type'] = SequenceType.FILEPART;
this.state = HeaderState.END;
return i + 1;
}
} else if (buffer[0] === 77) {
// 'MultipartFile'
for (let k = 0; k < MULTIPARTFILE_MARKER.length; ++k) {
if (buffer[k] !== MULTIPARTFILE_MARKER[k]) return this._a();
}
this.fields['type'] = SequenceType.MULTIPARTFILE;
} else {
return this._a();
} }
state = HeaderState.KEY; state = HeaderState.KEY;
pos = 0; pos = 0;
@@ -158,6 +232,7 @@ export class HeaderParser {
} }
private _a(): number { private _a(): number {
this.fields.type = SequenceType.INVALID;
this.state = HeaderState.ABORT; this.state = HeaderState.ABORT;
return -1; return -1;
} }
+3 -1
View File
@@ -3,6 +3,7 @@
* @license MIT * @license MIT
*/ */
import { IAddImageOpts } from 'Types';
import { ImageStorage } from './ImageStorage'; import { ImageStorage } from './ImageStorage';
/** /**
@@ -12,6 +13,7 @@ import { ImageStorage } from './ImageStorage';
* - Always uses scrolling mode (cursor advances with image) * - Always uses scrolling mode (cursor advances with image)
*/ */
export class IIPImageStorage { export class IIPImageStorage {
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
constructor( constructor(
private readonly _storage: ImageStorage private readonly _storage: ImageStorage
) {} ) {}
@@ -21,6 +23,6 @@ export class IIPImageStorage {
* Always uses scrolling mode — cursor advances past the image. * Always uses scrolling mode — cursor advances past the image.
*/ */
public addImage(img: HTMLCanvasElement | ImageBitmap): void { public addImage(img: HTMLCanvasElement | ImageBitmap): void {
this._storage.addImage(img, true); this._storage.addImage(img, this._addImageOpts);
} }
} }
+2 -1
View File
@@ -30,7 +30,8 @@ const TEST_IMAGES: [string, IMetrics][] = [
['iphone_hdr_YES.jpg', { mime: 'image/jpeg', width: 3264, height: 2448 }], ['iphone_hdr_YES.jpg', { mime: 'image/jpeg', width: 3264, height: 2448 }],
['nikon-e950.jpg', { mime: 'image/jpeg', width: 800, height: 600 }], ['nikon-e950.jpg', { mime: 'image/jpeg', width: 800, height: 600 }],
['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 }],
]; ];
+1 -6
View File
@@ -137,11 +137,6 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
// enable size reports // enable size reports
if (this._opts.enableSizeReports) { if (this._opts.enableSizeReports) {
// const windowOptions = terminal.getOption('windowOptions');
// windowOptions.getWinSizePixels = true;
// windowOptions.getCellSizePixels = true;
// windowOptions.getWinSizeChars = true;
// terminal.setOption('windowOptions', windowOptions);
const windowOps = terminal.options.windowOptions ?? {}; const windowOps = terminal.options.windowOptions ?? {};
windowOps.getWinSizePixels = true; windowOps.getWinSizePixels = true;
windowOps.getCellSizePixels = true; windowOps.getCellSizePixels = true;
@@ -260,7 +255,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
} }
private _report(s: string): void { private _report(s: string): void {
this._terminal?._core.coreService.triggerDataEvent(s); this._terminal?._core.input(s, false);
} }
private _decset(params: (number | number[])[]): boolean { private _decset(params: (number | number[])[]): boolean {
+1 -1
View File
@@ -146,7 +146,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
public clearLines(start: number, end: number, layer?: ImageLayer): void { public clearLines(start: number, end: number, layer?: ImageLayer): void {
const y = start * (this.dimensions?.css.cell.height || 0); const y = start * (this.dimensions?.css.cell.height || 0);
const w = this.dimensions?.css.canvas.width || 0; const w = this.dimensions?.css.canvas.width || 0;
const h = (++end - start) * (this.dimensions?.css.cell.height || 0); const h = (end + 1 - start) * (this.dimensions?.css.cell.height || 0);
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);
} }
+22 -13
View File
@@ -5,7 +5,11 @@
import { IDisposable } from '@xterm/xterm'; import { IDisposable } from '@xterm/xterm';
import { ImageRenderer } from './ImageRenderer'; import { ImageRenderer } from './ImageRenderer';
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types'; import {
ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec,
IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes,
UnderlineStyle, IAddImageOpts
} from './Types';
// fallback default cell size // fallback default cell size
@@ -235,13 +239,15 @@ export class ImageStorage implements IDisposable {
/** /**
* Method to add an image to the storage. * Method to add an image to the storage.
* @param img - The image to add (canvas or bitmap). * @param img - The image to add (canvas or bitmap).
* @param scrolling - When true, cursor advances with the image (lineFeed per row). * @param opts - Options for addImage:
* When false, image is placed at (0,0) and cursor is restored (DECSET 80 / sixel origin mode). * - scrolling: When true, cursor advances with the image.
* @param layer - Which canvas layer to render on ('top' or 'bottom'). * When false, image is placed at ORIGIN and cursor does not move.
* @param zIndex - Z-index for image layering within the same layer. * - layer: Which canvas layer to render on ('top' or 'bottom').
* - zIndex: Z-index for image layering within the same layer.
* - 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(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer = 'top', zIndex: number = 0): 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(img.width * img.height); this._evictOldest(img.width * img.height);
@@ -263,7 +269,7 @@ export class ImageStorage implements IDisposable {
let offset = originX; let offset = originX;
let tileCount = 0; let tileCount = 0;
if (!scrolling) { if (!opts.scrolling) {
buffer.x = 0; buffer.x = 0;
buffer.y = 0; buffer.y = 0;
offset = 0; offset = 0;
@@ -277,7 +283,7 @@ export class ImageStorage implements IDisposable {
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col); this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
tileCount++; tileCount++;
} }
if (scrolling) { if (opts.scrolling) {
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed(); if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
} else { } else {
if (++buffer.y >= termRows) break; if (++buffer.y >= termRows) break;
@@ -287,8 +293,12 @@ export class ImageStorage implements IDisposable {
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y); this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
// cursor positioning modes // cursor positioning modes
if (scrolling) { if (opts.scrolling) {
buffer.x = offset; if (opts.cursorPos === 'iip') {
buffer.x = Math.min(offset + cols, termCols);
} else {
buffer.x = offset;
}
} else { } else {
buffer.x = originX; buffer.x = originX;
buffer.y = originY; buffer.y = originY;
@@ -331,8 +341,8 @@ export class ImageStorage implements IDisposable {
marker: endMarker || undefined, marker: endMarker || undefined,
tileCount, tileCount,
bufferType: this._terminal.buffer.active.type, bufferType: this._terminal.buffer.active.type,
layer, layer: opts.layer,
zIndex zIndex: opts.zIndex
}; };
// finally add the image // finally add the image
@@ -455,7 +465,6 @@ export class ImageStorage implements IDisposable {
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) { if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
break; break;
} }
e = nextE;
count++; count++;
} }
col--; col--;
+4 -2
View File
@@ -4,7 +4,7 @@
*/ */
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage'; import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import { IImageAddonOptions, ITerminalExt } from './Types'; import { IImageAddonOptions, ITerminalExt, IAddImageOpts } from './Types';
import { ImageRenderer } from './ImageRenderer'; import { ImageRenderer } from './ImageRenderer';
/** /**
@@ -15,6 +15,7 @@ import { ImageRenderer } from './ImageRenderer';
* - advanceCursor for empty sixels carrying only height * - advanceCursor for empty sixels carrying only height
*/ */
export class SixelImageStorage { export class SixelImageStorage {
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'vt340' };
constructor( constructor(
private readonly _storage: ImageStorage, private readonly _storage: ImageStorage,
private readonly _opts: IImageAddonOptions, private readonly _opts: IImageAddonOptions,
@@ -27,7 +28,8 @@ export class SixelImageStorage {
* Cursor behavior depends on the sixelScrolling option (DECSET 80). * Cursor behavior depends on the sixelScrolling option (DECSET 80).
*/ */
public addImage(img: HTMLCanvasElement | ImageBitmap): void { public addImage(img: HTMLCanvasElement | ImageBitmap): void {
this._storage.addImage(img, this._opts.sixelScrolling); this._addImageOpts.scrolling = this._opts.sixelScrolling;
this._storage.addImage(img, this._addImageOpts);
} }
/** /**
+8
View File
@@ -100,6 +100,14 @@ export interface ICellSize {
} }
export type ImageLayer = 'top' | 'bottom'; export type ImageLayer = 'top' | 'bottom';
export type CursorPos = 'vt340' | 'iip';
export interface IAddImageOpts {
scrolling: boolean;
layer: ImageLayer;
zIndex: number;
cursorPos: CursorPos;
}
export interface IImageSpec { export interface IImageSpec {
orig: HTMLCanvasElement | ImageBitmap | undefined; orig: HTMLCanvasElement | ImageBitmap | undefined;
@@ -5,7 +5,7 @@
import { IDisposable } from '@xterm/xterm'; import { IDisposable } from '@xterm/xterm';
import { ImageStorage } from '../ImageStorage'; import { ImageStorage } from '../ImageStorage';
import { ImageLayer } from '../Types'; import { ImageLayer, IAddImageOpts } from '../Types';
import { IKittyImageData } from './KittyGraphicsTypes'; import { IKittyImageData } from './KittyGraphicsTypes';
// Kitty-specific image storage controller. // Kitty-specific image storage controller.
@@ -42,6 +42,7 @@ export class KittyImageStorage implements IDisposable {
this._images.delete(kittyId); this._images.delete(kittyId);
} }
}; };
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
constructor( constructor(
private readonly _storage: ImageStorage private readonly _storage: ImageStorage
@@ -98,7 +99,10 @@ export class KittyImageStorage implements IDisposable {
if (oldStorageId !== undefined) { if (oldStorageId !== undefined) {
this._storageIdToKittyId.delete(oldStorageId); this._storageIdToKittyId.delete(oldStorageId);
} }
const storageId = this._storage.addImage(image, scrolling, layer, zIndex); this._addImageOpts.scrolling = scrolling;
this._addImageOpts.layer = layer;
this._addImageOpts.zIndex = zIndex;
const storageId = this._storage.addImage(image, this._addImageOpts);
this._kittyIdToStorageId.set(kittyId, storageId); this._kittyIdToStorageId.set(kittyId, storageId);
this._storageIdToKittyId.set(storageId, kittyId); this._storageIdToKittyId.set(storageId, kittyId);
} }
@@ -290,6 +290,67 @@ test.describe('ImageAddon', () => {
}); });
}); });
test.describe('IIP support', () => {
test('size params is optional/informative', async () => {
// too small size param
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=5:${PALETTE_PNG_BASE64}\x07`);
deepStrictEqual(await getOrigSize(1), [640, 80]);
// no size aparm at all
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1:${PALETTE_QOI_BASE64}\x07`);
deepStrictEqual(await getOrigSize(2), [640, 80]);
});
test.skip('FilePart support - bytewise', async () => {
// opener
await ctx.proxy.write(`\x1b]1337;MultipartFile=inline=1\x07`);
// byte-wise
for (let i = 0; i < PALETTE_PNG_BASE64.length; ++i) {
await ctx.proxy.write(`\x1b]1337;FilePart=${PALETTE_PNG_BASE64[i]}\x07`);
}
// finalizer
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
deepStrictEqual(await getOrigSize(1), [640, 80]);
});
test('FilePart support - chunks', async () => {
// opener
await ctx.proxy.write(`\x1b]1337;MultipartFile=inline=1\x07`);
// chunks
for (let i = 0; i < PALETTE_PNG_BASE64.length; i += 256) {
await ctx.proxy.write(`\x1b]1337;FilePart=${PALETTE_PNG_BASE64.slice(i, i + 256)}\x07`);
}
// finalizer
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
deepStrictEqual(await getOrigSize(1), [640, 80]);
});
test('FilePart support - edgecases', async () => {
// multiple opener do not harm
for (let i = 0; i < 10; ++i) {
await ctx.proxy.write(`\x1b]1337;MultipartFile=inline=1\x07`);
}
// multiple finalizer do not harm
for (let i = 0; i < 5; ++i) {
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
}
// chunks w'o opener are ignored
for (let i = 0; i < PALETTE_PNG_BASE64.length; i += 256) {
await ctx.proxy.write(`\x1b]1337;FilePart=${PALETTE_PNG_BASE64.slice(i, i + 256)}\x07`);
}
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
pollFor(ctx.page, 'window.imageAddon._storage._images.size', 0);
});
test('FilePart support - other IIP sequence cancels chunked image', async () => {
// opener
await ctx.proxy.write(`\x1b]1337;MultipartFile=inline=1\x07`);
// chunks
for (let i = 0; i < PALETTE_PNG_BASE64.length; i += 256) {
await ctx.proxy.write(`\x1b]1337;FilePart=${PALETTE_PNG_BASE64.slice(i, i + 256)}\x07`);
}
// finalizer skipped
// write spinfox.png
await ctx.proxy.write(TESTDATA_IIP[1][0]);
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]);
+6 -12
View File
@@ -539,7 +539,6 @@ test.describe('Kitty Graphics Protocol', () => {
test.describe('Query support (a=q)', () => { test.describe('Query support (a=q)', () => {
test('responds with OK for capability query without payload', async () => { test('responds with OK for capability query without payload', async () => {
let response = '';
await ctx.page.evaluate(() => { await ctx.page.evaluate(() => {
(window as any).kittyResponse = ''; (window as any).kittyResponse = '';
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
@@ -548,12 +547,11 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write('\x1b_Gi=31,a=q;\x1b\\'); await ctx.proxy.write('\x1b_Gi=31,a=q;\x1b\\');
await timeout(100); await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse'); const response = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response, '\x1b_Gi=31;OK\x1b\\'); strictEqual(response, '\x1b_Gi=31;OK\x1b\\');
}); });
test('responds with OK for valid PNG query', async () => { test('responds with OK for valid PNG query', async () => {
let response = '';
await ctx.page.evaluate(() => { await ctx.page.evaluate(() => {
(window as any).kittyResponse = ''; (window as any).kittyResponse = '';
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
@@ -562,7 +560,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Gi=42,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); await ctx.proxy.write(`\x1b_Gi=42,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
await timeout(100); await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse'); const response = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response, '\x1b_Gi=42;OK\x1b\\'); strictEqual(response, '\x1b_Gi=42;OK\x1b\\');
}); });
@@ -578,7 +576,6 @@ test.describe('Kitty Graphics Protocol', () => {
}); });
test('responds with error for invalid base64', async () => { test('responds with error for invalid base64', async () => {
let response = '';
await ctx.page.evaluate(() => { await ctx.page.evaluate(() => {
(window as any).kittyResponse = ''; (window as any).kittyResponse = '';
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
@@ -587,12 +584,11 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write('\x1b_Gi=60,a=q,f=100;!!!invalid!!!\x1b\\'); await ctx.proxy.write('\x1b_Gi=60,a=q,f=100;!!!invalid!!!\x1b\\');
await timeout(100); await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse'); const response: string = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response.startsWith('\x1b_Gi=60;EINVAL:'), true); strictEqual(response.startsWith('\x1b_Gi=60;EINVAL:'), true);
}); });
test('responds with error for RGB data without dimensions', async () => { test('responds with error for RGB data without dimensions', async () => {
let response = '';
await ctx.page.evaluate(() => { await ctx.page.evaluate(() => {
(window as any).kittyResponse = ''; (window as any).kittyResponse = '';
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
@@ -601,7 +597,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write('\x1b_Gi=70,a=q,f=24;AAAA\x1b\\'); await ctx.proxy.write('\x1b_Gi=70,a=q,f=24;AAAA\x1b\\');
await timeout(100); await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse'); const response = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response, '\x1b_Gi=70;EINVAL:width and height required for raw pixel data\x1b\\'); strictEqual(response, '\x1b_Gi=70;EINVAL:width and height required for raw pixel data\x1b\\');
}); });
@@ -642,7 +638,6 @@ test.describe('Kitty Graphics Protocol', () => {
}); });
test('responds with EINVAL when both i and I keys are specified', async () => { test('responds with EINVAL when both i and I keys are specified', async () => {
let response = '';
await ctx.page.evaluate(() => { await ctx.page.evaluate(() => {
(window as any).kittyResponse = ''; (window as any).kittyResponse = '';
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
@@ -652,12 +647,11 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Gi=100,I=200,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); await ctx.proxy.write(`\x1b_Gi=100,I=200,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
await timeout(100); await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse'); const response = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response, '\x1b_Gi=100;EINVAL:cannot specify both i and I keys\x1b\\'); strictEqual(response, '\x1b_Gi=100;EINVAL:cannot specify both i and I keys\x1b\\');
}); });
test('responds with EINVAL for i+I conflict even without payload', async () => { test('responds with EINVAL for i+I conflict even without payload', async () => {
let response = '';
await ctx.page.evaluate(() => { await ctx.page.evaluate(() => {
(window as any).kittyResponse = ''; (window as any).kittyResponse = '';
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
@@ -667,7 +661,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write('\x1b_Gi=101,I=201,a=d\x1b\\'); await ctx.proxy.write('\x1b_Gi=101,I=201,a=d\x1b\\');
await timeout(100); await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse'); const response = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response, '\x1b_Gi=101;EINVAL:cannot specify both i and I keys\x1b\\'); strictEqual(response, '\x1b_Gi=101;EINVAL:cannot specify both i and I keys\x1b\\');
}); });
}); });
+4 -8
View File
@@ -31,15 +31,11 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0", "lru-cache": "^11.3.6",
"opentype.js": "^0.8.0" "opentype.js": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/lru-cache": "^5.1.0", "@types/opentype.js": "^1.3.9",
"@types/opentype.js": "^0.7.0", "font-finder": "^1.1.0"
"axios": "^1.15.2",
"font-finder": "^1.1.0",
"mkdirp": "0.5.5",
"yauzl": "^3.2.1"
} }
} }
@@ -1,5 +1,5 @@
import * as opentype from 'opentype.js'; import * as opentype from 'opentype.js';
import LRUCache = require('lru-cache'); import { LRUCache } from 'lru-cache';
import { IFont, ILigatureData, IFlattenedLookupTree, ILookupTree, IOptions } from './types'; import { IFont, ILigatureData, IFlattenedLookupTree, ILookupTree, IOptions } from './types';
import mergeTrees from './merge'; import mergeTrees from './merge';
@@ -23,8 +23,8 @@ class FontImpl implements IFont {
if (options.cacheSize > 0) { if (options.cacheSize > 0) {
this._cache = new LRUCache({ this._cache = new LRUCache({
max: options.cacheSize, maxSize: options.cacheSize,
length: ((val: ILigatureData | [number, number][], key: string) => key.length) as any sizeCalculation: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
}); });
} }
@@ -44,7 +44,6 @@ export default function mergeRange(ranges: [number, number][], newRangeStart: nu
// current range // current range
ranges[i - 1][1] = Math.max(newRangeEnd, range[1]); ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
ranges.splice(i, 1); ranges.splice(i, 1);
inRange = false;
return ranges; return ranges;
} }
// Case 7: New range extends from previous range past the // Case 7: New range extends from previous range past the
@@ -9,15 +9,18 @@ import { ITestContext, createTestContext, openTerminal } from '../../../test/pla
let ctx: ITestContext; let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
ctx.page.setViewportSize({ width: 1024, height: 768 });
await openTerminal(ctx);
});
test.afterAll(async () => await ctx.page.close());
test.describe('ProgressAddon', () => { test.describe('ProgressAddon', () => {
test.describe.configure({ mode: 'serial' });
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => {
await ctx?.page?.close();
});
test.beforeEach(async function(): Promise<any> { test.beforeEach(async function(): Promise<any> {
await ctx.page.evaluate(` await ctx.page.evaluate(`
window.progressStack = []; window.progressStack = [];
+3 -3
View File
@@ -174,7 +174,7 @@ export class SearchEngine {
this._terminal.clearSelection(); this._terminal.clearSelection();
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1; let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
let startCol = this._terminal.cols; const startCol = this._terminal.cols;
const isReverseSearch = true; const isReverseSearch = true;
this._lineCache.initLinesCache(); this._lineCache.initLinesCache();
@@ -186,14 +186,14 @@ export class SearchEngine {
let result: ISearchResult | undefined; let result: ISearchResult | undefined;
if (prevSelectedPos) { if (prevSelectedPos) {
searchPosition.startRow = startRow = prevSelectedPos.start.y; searchPosition.startRow = startRow = prevSelectedPos.start.y;
searchPosition.startCol = startCol = prevSelectedPos.start.x; searchPosition.startCol = prevSelectedPos.start.x;
if (cachedSearchTerm !== term) { if (cachedSearchTerm !== term) {
// Try to expand selection to right first. // Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false); result = this._findInLine(term, searchPosition, searchOptions, false);
if (!result) { if (!result) {
// If selection was not able to be expanded to the right, then try reverse search // If selection was not able to be expanded to the right, then try reverse search
searchPosition.startRow = startRow = prevSelectedPos.end.y; searchPosition.startRow = startRow = prevSelectedPos.end.y;
searchPosition.startCol = startCol = prevSelectedPos.end.x; searchPosition.startCol = prevSelectedPos.end.x;
} }
} }
} }
@@ -7,10 +7,10 @@
"rootDir": "..", "rootDir": "..",
"outDir": "../out-benchmark", "outDir": "../out-benchmark",
"types": ["../../../node_modules/@types/node"], "types": ["../../../node_modules/@types/node"],
"moduleResolution": "node", "moduleResolution": "nodenext",
"strict": false, "strict": false,
"target": "es2021", "target": "es2021",
"module": "commonjs", "module": "nodenext",
"paths": { "paths": {
"common/*": ["../../../src/common/*"], "common/*": ["../../../src/common/*"],
"browser/*": ["../../../src/browser/*"], "browser/*": ["../../../src/browser/*"],
@@ -4,10 +4,10 @@
"rootDir": "..", "rootDir": "..",
"outDir": "../out-benchmark", "outDir": "../out-benchmark",
"types": ["../../../node_modules/@types/node"], "types": ["../../../node_modules/@types/node"],
"moduleResolution": "node", "moduleResolution": "nodenext",
"strict": false, "strict": false,
"target": "es2015", "target": "es2015",
"module": "commonjs", "module": "nodenext",
"paths": { "paths": {
"common/*": ["../../../src/common/*"], "common/*": ["../../../src/common/*"],
"browser/*": ["../../../src/browser/*"], "browser/*": ["../../../src/browser/*"],
@@ -78,7 +78,22 @@ test.describe('WebFontsAddon', () => {
const data = await ctx.page.evaluate(` const data = await ctx.page.evaluate(`
document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/fonts/kongtext.regular.ttf) format('truetype'); unicode-range: U+00A0-00FF}", 0); document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/fonts/kongtext.regular.ttf) format('truetype'); unicode-range: U+00A0-00FF}", 0);
`); `);
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]); let fonts = await getDocumentFonts();
// safari may have loaded the font, firefox & chrome never load it
if (browser.browserType().name() === 'webkit') {
try {
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loading' }]);
} catch (e) {
try {
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loaded' }]);
} catch (e) {
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
}
}
} else {
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
}
// broken case: webfont in ctor without addon usage // broken case: webfont in ctor without addon usage
await ctx.page.evaluate(` await ctx.page.evaluate(`
@@ -86,15 +101,20 @@ test.describe('WebFontsAddon', () => {
window.helperTerm.open(term.element); window.helperTerm.open(term.element);
`); `);
// safari loads the font, firefox & chrome dont fonts = await getDocumentFonts();
// safari might load the font, firefox & chrome dont
if (browser.browserType().name() === 'webkit') { if (browser.browserType().name() === 'webkit') {
try { try {
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loading' }]); deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loading' }]);
} catch (e) { } catch (e) {
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]); try {
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loaded' }]);
} catch (e) {
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
}
} }
} else { } else {
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]); deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
} }
// good case: addon fixes layout for webfont in ctor // good case: addon fixes layout for webfont in ctor
@@ -5,7 +5,6 @@
"lib": [ "lib": [
"es2021", "es2021",
], ],
// "downlevelIteration": true,
"rootDir": ".", "rootDir": ".",
"outDir": "../out-test", "outDir": "../out-test",
"sourceMap": true, "sourceMap": true,
@@ -113,8 +113,8 @@ export class LinkComputer {
let line: IBufferLine | undefined; let line: IBufferLine | undefined;
let topIdx = lineIndex; let topIdx = lineIndex;
let bottomIdx = lineIndex; let bottomIdx = lineIndex;
let length = 0; let length;
let content = ''; let content;
const lines: string[] = []; const lines: string[] = [];
if ((line = terminal.buffer.active.getLine(lineIndex))) { if ((line = terminal.buffer.active.getLine(lineIndex))) {
@@ -5,7 +5,6 @@
"lib": [ "lib": [
"es2021", "es2021",
], ],
// "downlevelIteration": true,
"rootDir": ".", "rootDir": ".",
"outDir": "../out-test", "outDir": "../out-test",
"sourceMap": true, "sourceMap": true,
+2 -2
View File
@@ -427,8 +427,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
let line: IBufferLine; let line: IBufferLine;
let joinedRanges: [number, number][]; let joinedRanges: [number, number][];
let isJoined: boolean; let isJoined: boolean;
let skipJoinedCheckUntilX: number = 0; let skipJoinedCheckUntilX: number;
let isValidJoinRange: boolean = true; let isValidJoinRange: boolean;
let lastCharX: number; let lastCharX: number;
let range: [number, number]; let range: [number, number];
let isCursorRow: boolean; let isCursorRow: boolean;
-1
View File
@@ -25,7 +25,6 @@
] ]
}, },
"strict": true, "strict": true,
"downlevelIteration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"types": [ "types": [
"../../../node_modules/@types/mocha" "../../../node_modules/@types/mocha"
@@ -65,7 +65,8 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
const dtIip = document.createElement('dt'); const dtIip = document.createElement('dt');
dtIip.textContent = 'IIP (iTerm)'; dtIip.textContent = 'IIP (iTerm)';
dlIip.appendChild(dtIip); dlIip.appendChild(dtIip);
this._addDdWithButton(dlIip, 'image-demo3', 'palette'); this._addDdWithButton(dlIip, 'image-demo3', 'palette (png File)');
this._addDdWithButton(dlIip, 'image-demo4', 'dice (qoi MultipartFile)');
container.appendChild(dlIip); container.appendChild(dlIip);
// Kitty demos // Kitty demos
@@ -141,6 +142,21 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
this._terminal.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`); this._terminal.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`);
}); });
const iipDemoMulti = (url: string) => () => fetch(url)
.then(resp => resp.arrayBuffer())
.then(buffer => {
const data = new Uint8Array(buffer);
let sdata = '';
for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]);
const encoded = btoa(sdata);
this._terminal.write('\r\n');
this._terminal.write(`\x1b]1337;MultipartFile=inline=1\x1b\\`);
for (let i = 0; i < encoded.length; i += 100) {
this._terminal.write(`\x1b]1337;FilePart=${encoded.slice(i, i + 100)}\x1b\\`);
}
this._terminal.write(`\x1b]1337;FileEnd\x1b\\`);
});
const kittyDemo = (url: string) => () => fetch(url) const kittyDemo = (url: string) => () => fetch(url)
.then(resp => resp.arrayBuffer()) .then(resp => resp.arrayBuffer())
.then(buffer => { .then(buffer => {
@@ -158,6 +174,8 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
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/jerch/node-sixel/master/palette.png')); iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
document.getElementById('image-demo4')!.addEventListener('click',
iipDemoMulti('https://raw.githubusercontent.com/jerch/xterm-wasm-parts/master/fixtures/qoi/dice.qoi'));
document.getElementById('image-demo-kitty1')!.addEventListener('click', document.getElementById('image-demo-kitty1')!.addEventListener('click',
kittyDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png')); kittyDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
@@ -176,7 +194,7 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
// } // }
// } // }
const pos = (this._terminal as any)._core._mouseService!.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;
const canvas = ev.shiftKey const canvas = ev.shiftKey
+1
View File
@@ -2,6 +2,7 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es2021", "target": "es2021",
"types": ["node"],
"outDir": "../out-demo/client", "outDir": "../out-demo/client",
"rootDir": ".", "rootDir": ".",
"sourceMap": true, "sourceMap": true,
+1 -1
View File
@@ -118,7 +118,7 @@ function startServer(): void {
}); });
appWs.ws('/terminals/:pid', function (ws, req) { appWs.ws('/terminals/:pid', function (ws, req) {
const term = terminals[parseInt(req.params.pid)]; const term = terminals[parseInt(Array.isArray(req.params.pid) ? req.params.pid[0] : req.params.pid)];
console.log('Connected to terminal ' + term.pid); console.log('Connected to terminal ' + term.pid);
temporaryDisposable[term.pid].dispose(); temporaryDisposable[term.pid].dispose();
delete temporaryDisposable[term.pid]; delete temporaryDisposable[term.pid];
+1 -1
View File
@@ -37,7 +37,7 @@ export default tseslint.config(
'@stylistic/comma-spacing': ['warn', { before: false, after: true }], '@stylistic/comma-spacing': ['warn', { before: false, after: true }],
'@stylistic/indent': ['warn', 2], '@stylistic/indent': ['warn', 2],
'@stylistic/semi': ['warn', 'always'], '@stylistic/semi': ['warn', 'always'],
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }], '@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: 'always' }],
'@stylistic/member-delimiter-style': ['warn', { '@stylistic/member-delimiter-style': ['warn', {
multiline: { delimiter: 'semi', requireLast: true }, multiline: { delimiter: 'semi', requireLast: true },
singleline: { delimiter: 'comma', requireLast: false } singleline: { delimiter: 'comma', requireLast: false }
+1 -1
View File
@@ -17,7 +17,7 @@ export default tseslint.config(
rules: { rules: {
'@stylistic/indent': ['warn', 2], '@stylistic/indent': ['warn', 2],
'@stylistic/semi': ['warn', 'always'], '@stylistic/semi': ['warn', 'always'],
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }], '@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: 'always' }],
'@stylistic/member-delimiter-style': ['warn', { '@stylistic/member-delimiter-style': ['warn', {
multiline: { delimiter: 'semi', requireLast: true }, multiline: { delimiter: 'semi', requireLast: true },
singleline: { delimiter: 'comma', requireLast: false } singleline: { delimiter: 'comma', requireLast: false }
+2622 -2292
View File
File diff suppressed because it is too large Load Diff
+25 -24
View File
@@ -75,40 +75,41 @@
"prepublishOnly": "npm run package" "prepublishOnly": "npm run package"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1",
"@lunapaint/png-codec": "^0.2.0", "@lunapaint/png-codec": "^0.2.0",
"@playwright/test": "^1.57.0", "@playwright/test": "^1.59.1",
"@stylistic/eslint-plugin": "^4.4.1", "@stylistic/eslint-plugin": "^5.10.0",
"@types/chai": "^4.2.22", "@types/chai": "^5.2.3",
"@types/express": "4", "@types/express": "^5.0.6",
"@types/express-ws": "^3.0.1", "@types/express-ws": "^3.0.1",
"@types/jsdom": "^27.0.0", "@types/jsdom": "^28.0.1",
"@types/mocha": "^9.0.0", "@types/mocha": "^10.0.10",
"@types/node": "^22.19.3", "@types/node": "^22.19.3",
"@types/webpack": "^5.28.0", "@types/webpack": "^5.28.0",
"@types/ws": "^8.2.0", "@types/ws": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^8.50.1", "@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.50.1", "@typescript-eslint/parser": "^8.59.2",
"@typescript/native-preview": "^7.0.0-dev.20260213.1", "@typescript/native-preview": "^7.0.0-dev.20260509.2",
"chai": "^4.3.4", "chai": "^6.2.2",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"esbuild": "~0.25.2", "esbuild": "^0.28.0",
"eslint": "^9.39.2", "eslint": "^10.3.0",
"eslint-plugin-jsdoc": "^50.8.0", "eslint-plugin-jsdoc": "^62.9.0",
"express": "^4.19.2", "express": "^5.2.1",
"express-ws": "^5.0.2", "express-ws": "^5.0.2",
"jsdom": "^27.3.0", "jsdom": "^29.1.1",
"mocha": "^10.1.0", "mocha": "^11.7.5",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"node-pty": "^1.2.0-beta.9", "node-pty": "^1.2.0-beta.9",
"nyc": "^17.1.0", "nyc": "^18.0.0",
"source-map-loader": "^3.0.0", "source-map-loader": "^5.0.0",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.20",
"ts-loader": "^9.3.1", "ts-loader": "^9.5.7",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.50.1", "typescript-eslint": "^8.59.2",
"webpack": "^5.61.0", "webpack": "^5.106.2",
"webpack-cli": "^4.9.1", "webpack-cli": "^7.0.2",
"ws": "^8.2.3", "ws": "^8.20.0",
"xterm-benchmark": "^0.3.1" "xterm-benchmark": "^0.3.2"
} }
} }
+5 -4
View File
@@ -37,7 +37,7 @@ import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'
import { CoreBrowserService } from 'browser/services/CoreBrowserService'; import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { LinkProviderService } from 'browser/services/LinkProviderService'; import { LinkProviderService } from 'browser/services/LinkProviderService';
import { MouseCoordsService } from 'browser/services/MouseCoordsService'; import { MouseCoordsService } from 'browser/services/MouseCoordsService';
import { MouseService } from 'browser/services/MouseService'; import { MouseEventCssClasses, MouseService } from 'browser/services/MouseService';
import { RenderService } from 'browser/services/RenderService'; import { RenderService } from 'browser/services/RenderService';
import { SelectionService } from 'browser/services/SelectionService'; import { SelectionService } from 'browser/services/SelectionService';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
@@ -212,7 +212,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
if (!this._themeService) return; if (!this._themeService) return;
for (const req of event) { for (const req of event) {
let acc: 'foreground' | 'background' | 'cursor' | 'ansi'; let acc: 'foreground' | 'background' | 'cursor' | 'ansi';
let ident = ''; let ident: string;
switch (req.index) { switch (req.index) {
case SpecialColorIndex.FOREGROUND: // OSC 10 | 110 case SpecialColorIndex.FOREGROUND: // OSC 10 | 110
acc = 'foreground'; acc = 'foreground';
@@ -618,11 +618,12 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._register(addDisposableListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e))); this._register(addDisposableListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
// apply mouse event classes set by escape codes before terminal was attached // apply mouse event classes set by escape codes before terminal was attached
if (this.mouseStateService.areMouseEventsActive) { if (this.mouseStateService.areMouseEventsActive && !this.options.mouseEventsRequireAlt) {
this._selectionService.disable(); this._selectionService.disable();
this.element.classList.add('enable-mouse-events'); this.element.classList.add(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
} else { } else {
this._selectionService.enable(); this._selectionService.enable();
this.element.classList.remove(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
} }
if (this.options.screenReaderMode) { if (this.options.screenReaderMode) {
+173
View File
@@ -0,0 +1,173 @@
# Browser folder rearchitecture
Composite TypeScript projects with enforced dependencies, mirroring the layered layout planned for `src/common/`.
## Platform layer rule
A module is **platform** only if **no class** in that project has a **service** injected via its constructor. That includes:
- Common services (`@IBufferService`, `@IOptionsService`, `@ICoreService`, `@IInstantiationService`, etc.)
- Browser services (`@ICoreBrowserService`, `@IRenderService`, `@IThemeService`, `@ICharSizeService`, …)
Passing a service into a **function** (e.g. `moveToCellSequence(…, bufferService)`) is fine. `new TextBlinkStateManager(…, coreBrowserService, optionsService)` is not — that class belongs in integration.
Interface-only modules (`browser/selection/Types.ts`, `renderer/shared/Types.ts`) are platform. Implementations that take services are not.
## Dependency graph (corrected)
```mermaid
flowchart BT
subgraph platform["Platform layer — no ctor service injection"]
direction LR
base["browser/base"]
scrollable["browser/scrollable"]
input["browser/input"]
selectionTypes["browser/selection/types"]
renderCore["browser/renderer/core"]
decorStore["browser/decorations/store"]
scrollable --> base
input --> base
selectionTypes --> base
renderCore --> base
decorStore --> base
end
subgraph integration["Integration layer — DI and wiring"]
direction LR
services["browser/services"]
renderer["browser/renderer"]
decorations["browser/decorations"]
terminal["browser/terminal"]
services --> base
services --> input
services --> selectionTypes
services --> renderCore
services --> decorStore
renderer --> services
renderer --> renderCore
renderer --> base
decorations --> services
decorations --> decorStore
decorations --> renderCore
terminal --> services
terminal --> renderer
terminal --> decorations
terminal --> input
terminal --> scrollable
terminal --> base
end
subgraph api_layer["API layer"]
public["browser/public"]
end
integration --> platform
api_layer --> integration
public --> terminal
```
`browser/renderer` is **not** platform: it owns `DomRenderer`, `DomRendererRowFactory`, and `TextBlinkStateManager`, all of which take injected services.
## Constructor audit (current tree)
### Platform-eligible
| Location | Type | Notes |
| --- | --- | --- |
| `Dom.ts`, `Types.ts`, `LocalizableStrings.ts`, `ColorContrastCache.ts`, `TimeBasedDebouncer.ts`, `shared/` | classes / functions | no DI |
| `scrollable/**` | widgets | `touch.ts` uses `Dom` only |
| `input/Mouse.ts` | functions | pure DOM math |
| `input/MoveToCell.ts` | functions | `IBufferService` passed as argument, not ctor-injected |
| `selection/Types.ts` | interfaces | no classes with DI |
| `renderer/shared/Constants.ts`, `RendererUtils.ts`, `Types.ts`, `SelectionRenderModel.ts` | classes | no DI |
| `renderer/dom/WidthCache.ts` | class | canvas factory only, no services |
| `decorations/ColorZoneStore.ts` | class | common types only, no DI |
### Integration-only (ctor service injection)
| Location | Injected services (representative) |
| --- | --- |
| `selection/SelectionModel.ts` | `IBufferService` |
| `input/CompositionHelper.ts` | `IBufferService`, `IOptionsService`, `ICoreService`, `IRenderService` |
| `renderer/shared/TextBlinkStateManager.ts` | `ICoreBrowserService`, `IOptionsService` |
| `renderer/dom/DomRendererRowFactory.ts` | `ICharacterJoinerService`, `IOptionsService`, `ICoreBrowserService`, `ICoreService`, `IDecorationService`, `IThemeService` |
| `renderer/dom/DomRenderer.ts` | `IInstantiationService`, `ICharSizeService`, `IOptionsService`, `IBufferService`, `ICoreService`, `ICoreBrowserService`, `IThemeService`; creates `DomRendererRowFactory` via `IInstantiationService` |
| `renderer/dom/DomRenderer` inner `CursorBlinkStateManager` | `ICoreBrowserService` |
| `services/**` | common + browser services (all `@…` ctor params) |
| `RenderDebouncer.ts` | `ICoreBrowserService` |
| `decorations/BufferDecorationRenderer.ts`, `OverviewRulerRenderer.ts` | `IBufferService`, `ICoreBrowserService`, `IDecorationService`, `IRenderService`, `IThemeService`, … |
| `Viewport.ts`, `Linkifier.ts`, `AccessibilityManager.ts`, `OscLinkProvider.ts` | multiple common + browser services |
| `CoreBrowserTerminal.ts` | orchestrates service registration and renderer construction |
`Clipboard.ts` has no class ctor DI; handlers receive `ISelectionService` at call sites — keep with `terminal`.
## Target package layout
### Platform
| Project | Contents |
| --- | --- |
| `browser/base` | `Dom`, `Types`, `LocalizableStrings`, `ColorContrastCache`, `TimeBasedDebouncer`, `shared/`; browser **service interfaces** (extracted from `services/Services.ts`) |
| `browser/scrollable` | `scrollable/**` |
| `browser/input` | `Mouse.ts`, `MoveToCell.ts` only |
| `browser/selection/types` | `selection/Types.ts` (or merge into `base` if preferred) |
| `browser/renderer/core` | `renderer/shared/{Constants,RendererUtils,Types,SelectionRenderModel}.ts`, `renderer/dom/WidthCache.ts` |
| `browser/decorations/store` | `ColorZoneStore.ts` |
`SelectionModel` moves to **`browser/services`** (only consumer is `SelectionService`).
`CompositionHelper` moves to **`browser/terminal`** (or `browser/input/ime` as integration sibling).
`TextBlinkStateManager`, `DomRenderer`, `DomRendererRowFactory` move to **`browser/renderer`** (integration), depending on **`browser/services`**.
### Integration
| Project | Contents |
| --- | --- |
| `browser/services` | `services/**`, `selection/SelectionModel.ts`, `RenderDebouncer.ts` |
| `browser/renderer` | `TextBlinkStateManager.ts`, `renderer/dom/DomRenderer.ts`, `DomRendererRowFactory.ts` (+ tests) |
| `browser/decorations` | `BufferDecorationRenderer.ts`, `OverviewRulerRenderer.ts` |
| `browser/terminal` | `CoreBrowserTerminal.ts`, `Viewport.ts`, `Linkifier.ts`, `Clipboard.ts`, `AccessibilityManager.ts`, `OscLinkProvider.ts`, `CompositionHelper.ts` |
### API
| Project | Contents |
| --- | --- |
| `browser/public` | `public/Terminal.ts` |
## Build order
1. `base`, `scrollable`, `input`, `selection/types`, `renderer/core`, `decorations/store`
2. `services`
3. `renderer`, `decorations`
4. `terminal`
5. `public`
## Why `renderer/dom` is not platform
`DomRenderer` (abbreviated):
```ts
constructor(
…,
@IInstantiationService instantiationService: IInstantiationService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IThemeService private readonly _themeService: IThemeService
) {
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document);
this._textBlinkStateManager = this._register(new TextBlinkStateManager(…, this._coreBrowserService, this._optionsService));
}
```
`DomRendererRowFactory` similarly injects `ICharacterJoinerService`, `ICoreBrowserService`, `IThemeService`, and other services. So **`renderer/dom` → `browser/services`** (and common services) today; the corrected graph places the whole DOM renderer stack in integration, with only `WidthCache` left in `renderer/core`.
## Parity with `common/`
Common platform modules contain algorithms and data structures without terminal service wiring. Browser platform modules are the same: DOM/scroll utilities, pointer math, renderer **primitives** (`WidthCache`, selection render geometry), and shared types — not renderers or IME helpers that pull `IRenderService` / `IThemeService` from DI.
+2 -2
View File
@@ -14,7 +14,7 @@ import { IDisposable } from '@xterm/xterm';
const COLS = 80; const COLS = 80;
const ROWS = 25; const ROWS = 25;
const escapeSequenceFilesDir = path.join(__dirname, '../../fixtures/escape_sequence_files'); const escapeSequenceFilesDir = path.join(__dirname, '../../test/fixtures/escape_sequence_files');
const TESTFILES = fs.readdirSync(escapeSequenceFilesDir) const TESTFILES = fs.readdirSync(escapeSequenceFilesDir)
.filter(f => f.endsWith('.in')) .filter(f => f.endsWith('.in'))
.map(f => path.join(escapeSequenceFilesDir, f)); .map(f => path.join(escapeSequenceFilesDir, f));
@@ -125,7 +125,7 @@ function formatError(input: string, output: string, expected: string): string {
// simple debug output of terminal cells // simple debug output of terminal cells
function terminalToString(term: CoreBrowserTerminal): string { function terminalToString(term: CoreBrowserTerminal): string {
let result = ''; let result = '';
let lineText = ''; let lineText: string;
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
lineText = term.buffer.lines.get(line)!.translateToString(true); lineText = term.buffer.lines.get(line)!.translateToString(true);
// rtrim empty cells as xterm does // rtrim empty cells as xterm does
@@ -90,14 +90,14 @@ export class DomRendererRowFactory {
let charElement: HTMLSpanElement | undefined; let charElement: HTMLSpanElement | undefined;
let cellAmount = 0; let cellAmount = 0;
let text = ''; let text = '';
let i = 0; let i;
let oldBg = 0; let oldBg = 0;
let oldFg = 0; let oldFg = 0;
let oldExt = 0; let oldExt = 0;
let oldLinkHover: number | boolean = false; let oldLinkHover: number | boolean = false;
let oldSpacing = 0; let oldSpacing = 0;
let oldIsInSelection: boolean = false; let oldIsInSelection: boolean = false;
let spacing = 0; let spacing;
let skipJoinedCheckUntilX = 0; let skipJoinedCheckUntilX = 0;
const classes: string[] = []; const classes: string[] = [];
@@ -231,6 +231,7 @@ export class DomRendererRowFactory {
} }
charElement = this._document.createElement('span'); charElement = this._document.createElement('span');
cellAmount = 0; cellAmount = 0;
// eslint-disable-next-line no-useless-assignment
text = ''; text = '';
} }
} }
+1 -1
View File
@@ -110,7 +110,7 @@ export class WidthCache implements IDisposable {
* `variant` denotes the font variant to be used. * `variant` denotes the font variant to be used.
*/ */
public get(c: string, bold: boolean | number, italic: boolean | number): number { public get(c: string, bold: boolean | number, italic: boolean | number): number {
let cp = 0; let cp: number;
if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) { if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) {
if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) { if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) {
return this._flat[cp]; return this._flat[cp];
+1 -1
View File
@@ -367,7 +367,7 @@ export class Gesture extends Disposable {
private _dispatchEvent(event: IGestureEvent): void { private _dispatchEvent(event: IGestureEvent): void {
if (event.type === EventType.TAP) { if (event.type === EventType.TAP) {
const currentTime = (new Date()).getTime(); const currentTime = (new Date()).getTime();
let setTapCount = 0; let setTapCount;
if (currentTime - this._lastSetTapCountTime > Gesture._clearTapCountTime) { if (currentTime - this._lastSetTapCountTime > Gesture._clearTapCountTime) {
setTapCount = 1; setTapCount = 1;
} else { } else {
@@ -285,7 +285,7 @@ function substringJoiner(substring: string): (sequence: string) => [number, numb
return (sequence: string): [number, number][] => { return (sequence: string): [number, number][] => {
const ranges: [number, number][] = []; const ranges: [number, number][] = [];
let searchIndex = 0; let searchIndex = 0;
let matchIndex = -1; let matchIndex;
while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) { while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) {
const matchEndIndex = matchIndex + substring.length; const matchEndIndex = matchIndex + substring.length;
+141 -1
View File
@@ -3,10 +3,11 @@
* @license MIT * @license MIT
*/ */
import { assert } from 'chai'; import { assert } from 'chai';
import { MouseService } from 'browser/services/MouseService'; import { AltMouseCursorController, MouseEventCssClasses, MouseService } from 'browser/services/MouseService';
import { MouseStateService } from 'common/services/MouseStateService'; import { MouseStateService } from 'common/services/MouseStateService';
import { CoreMouseAction, CoreMouseButton } from 'common/Types'; import { CoreMouseAction, CoreMouseButton } from 'common/Types';
import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
import { OptionsService } from 'common/services/OptionsService';
import { MockCoreBrowserService, MockRenderService, MockSelectionService } from 'browser/TestUtils.test'; import { MockCoreBrowserService, MockRenderService, MockSelectionService } from 'browser/TestUtils.test';
function toBytes(s: string | undefined): number[] { function toBytes(s: string | undefined): number[] {
@@ -42,6 +43,36 @@ const logService: ILogService = {
error: () => {} error: () => {}
} as any; } as any;
class TestSelectionService extends MockSelectionService {
public enableCount = 0;
public disableCount = 0;
public override enable(): void {
this.enableCount++;
}
public override disable(): void {
this.disableCount++;
}
public override shouldForceSelection(): boolean {
return false;
}
}
function createTestMouseTargetElement(): HTMLElement {
const classes = new Set<string>();
return {
classList: {
add: (className: string) => classes.add(className),
remove: (className: string) => classes.delete(className),
contains: (className: string) => classes.has(className)
},
addEventListener: () => {},
removeEventListener: () => {}
} as any;
}
describe('MouseService _triggerMouseEvent', () => { describe('MouseService _triggerMouseEvent', () => {
let mouseService: MouseService; let mouseService: MouseService;
let mouseStateService: MouseStateService; let mouseStateService: MouseStateService;
@@ -211,3 +242,112 @@ describe('MouseService _triggerMouseEvent', () => {
reports = []; reports = [];
}); });
}); });
describe('MouseService mouseEventsRequireAlt', () => {
it('should update selection state and cursor class when toggled while mouse events are active', () => {
const mouseStateService = new MouseStateService();
const optionsService = new OptionsService({});
const selectionService = new TestSelectionService();
const mouseService = new MouseService(
new MockRenderService(),
{
getMouseReportCoords: (_ev: MouseEvent, _el: HTMLElement) => ({ col: 0, row: 0, x: 0, y: 0 })
} as any,
mouseStateService,
{
triggerDataEvent: () => {},
triggerBinaryEvent: () => {},
decPrivateModes: { applicationCursorKeys: false }
} as any,
bufferService,
optionsService,
selectionService,
logService,
new MockCoreBrowserService()
);
const element = createTestMouseTargetElement();
const screenElement = createTestMouseTargetElement();
const document = {
addEventListener: () => {},
removeEventListener: () => {}
} as any;
mouseService.bindMouse({
element,
screenElement,
document
}, disposable => disposable, () => {});
mouseStateService.activeProtocol = 'ANY';
assert.isTrue(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
assert.equal(selectionService.disableCount, 1);
optionsService.options.mouseEventsRequireAlt = true;
assert.isFalse(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
assert.equal(selectionService.enableCount, 2);
optionsService.options.mouseEventsRequireAlt = false;
assert.isTrue(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
assert.equal(selectionService.disableCount, 2);
});
it('should strip alt modifier from forwarded mouse reports', () => {
const mouseStateService = new MouseStateService();
mouseStateService.activeProtocol = 'ANY';
mouseStateService.activeEncoding = 'SGR';
const optionsService = new OptionsService({ mouseEventsRequireAlt: true });
const reports: string[] = [];
const mouseService = new MouseService(
new MockRenderService(),
{
getMouseReportCoords: () => ({ col: 0, row: 0, x: 0, y: 0 })
} as any,
mouseStateService,
{
triggerDataEvent: (data: string) => reports.push(data),
triggerBinaryEvent: () => {},
decPrivateModes: { applicationCursorKeys: false }
} as any,
bufferService,
optionsService,
new TestSelectionService(),
logService,
new MockCoreBrowserService()
);
const ctx = {
target: {
screenElement: createTestMouseTargetElement()
},
requestedEvents: {}
} as any;
const sent = (mouseService as any)._sendEvent(ctx, {
type: 'mousedown',
button: 0,
altKey: true,
ctrlKey: false,
shiftKey: false
} as MouseEvent);
assert.isTrue(sent);
assert.deepEqual(reports, ['\x1b[<0;1;1M']);
});
it('should toggle enable-mouse-events class when alt modifier changes', () => {
const element = createTestMouseTargetElement();
const altMouseCursor = new AltMouseCursorController(element, {
addEventListener: () => {},
removeEventListener: () => {}
} as any, () => true);
const sync = (altHeld: boolean) => altMouseCursor.syncFromModifier({
getModifierState: (key: string) => key === 'Alt' && altHeld
} as KeyboardEvent);
assert.isFalse(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
sync(true);
assert.isTrue(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
sync(false);
assert.isFalse(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
});
});
+109 -6
View File
@@ -7,12 +7,16 @@ import { addDisposableListener } from 'browser/Dom';
import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from 'common/Types'; import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from 'common/Types';
import { C0 } from 'common/data/EscapeSequences'; import { C0 } from 'common/data/EscapeSequences';
import { toDisposable } from 'common/Lifecycle'; import { DisposableStore, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services'; import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
import { Gesture, EventType as GestureEventType, IGestureEvent } from 'browser/scrollable/touch'; import { Gesture, EventType as GestureEventType, IGestureEvent } from 'browser/scrollable/touch';
type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>; type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>;
export const enum MouseEventCssClasses {
ENABLE_MOUSE_EVENTS = 'enable-mouse-events'
}
interface IMouseBindContext { interface IMouseBindContext {
readonly target: IMouseServiceTarget; readonly target: IMouseServiceTarget;
readonly focus: () => void; readonly focus: () => void;
@@ -25,6 +29,7 @@ export class MouseService implements IMouseService {
private _lastEvent: ICoreMouseEvent | null = null; private _lastEvent: ICoreMouseEvent | null = null;
private _wheelPartialScroll: number = 0; private _wheelPartialScroll: number = 0;
private _touchScrollAccumulator: number = 0; private _touchScrollAccumulator: number = 0;
private _altMouseCursor: AltMouseCursorController | undefined;
constructor( constructor(
@IRenderService private readonly _renderService: IRenderService, @IRenderService private readonly _renderService: IRenderService,
@@ -63,9 +68,20 @@ export class MouseService implements IMouseService {
mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent), mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent),
mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent) mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent)
}; };
this._altMouseCursor = new AltMouseCursorController(
element,
document,
() => this._mouseStateService.areMouseEventsActive
&& !!this._optionsService.rawOptions.mouseEventsRequireAlt
);
register(this._altMouseCursor);
register(this._mouseStateService.onProtocolChange(events => { register(this._mouseStateService.onProtocolChange(events => {
this._handleProtocolChange(ctx, eventListeners, events); this._handleProtocolChange(ctx, eventListeners, events);
})); }));
register(this._optionsService.onSpecificOptionChange('mouseEventsRequireAlt', () => {
this._syncMouseModeState(element);
this._altMouseCursor?.sync();
}));
// force initial onProtocolChange so we dont miss early mouse requests // force initial onProtocolChange so we dont miss early mouse requests
this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol; this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol;
@@ -153,6 +169,19 @@ export class MouseService implements IMouseService {
return false; return false;
} }
if (but !== CoreMouseButton.WHEEL
&& this._optionsService.rawOptions.mouseEventsRequireAlt
&& this._mouseStateService.areMouseEventsActive
&& !ev.altKey) {
return false;
}
// Alt is only used locally to gate mouse passthrough; do not forward it to the
// application (e.g. tmux ignores alt-modified mouse reports).
const stripAltFromReport = but !== CoreMouseButton.WHEEL
&& this._optionsService.rawOptions.mouseEventsRequireAlt
&& this._mouseStateService.areMouseEventsActive;
return this._triggerMouseEvent({ return this._triggerMouseEvent({
col: pos.col, col: pos.col,
row: pos.row, row: pos.row,
@@ -161,7 +190,7 @@ export class MouseService implements IMouseService {
button: but, button: but,
action, action,
ctrl: ev.ctrlKey, ctrl: ev.ctrlKey,
alt: ev.altKey, alt: stripAltFromReport ? false : ev.altKey,
shift: ev.shiftKey shift: ev.shiftKey
}); });
} }
@@ -353,6 +382,21 @@ export class MouseService implements IMouseService {
this._touchScrollAccumulator = 0; this._touchScrollAccumulator = 0;
} }
private _syncMouseModeState(element: HTMLElement): void {
if (this._mouseStateService.areMouseEventsActive) {
if (this._optionsService.rawOptions.mouseEventsRequireAlt) {
this._altMouseCursor?.resetClass();
this._selectionService.enable();
} else {
element.classList.add(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
this._selectionService.disable();
}
} else {
element.classList.remove(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
this._selectionService.enable();
}
}
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void { private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
const { element, document } = ctx.target; const { element, document } = ctx.target;
const { requestedEvents } = ctx; const { requestedEvents } = ctx;
@@ -361,13 +405,11 @@ export class MouseService implements IMouseService {
if (this._optionsService.rawOptions.logLevel === 'debug') { if (this._optionsService.rawOptions.logLevel === 'debug') {
this._logService.debug('Binding to mouse events:', this._explainEvents(events)); this._logService.debug('Binding to mouse events:', this._explainEvents(events));
} }
element.classList.add('enable-mouse-events');
this._selectionService.disable();
} else { } else {
this._logService.debug('Unbinding from mouse events.'); this._logService.debug('Unbinding from mouse events.');
element.classList.remove('enable-mouse-events');
this._selectionService.enable();
} }
this._syncMouseModeState(element);
this._altMouseCursor?.sync();
// add/remove handlers from requestedEvents // add/remove handlers from requestedEvents
if (!(events & CoreMouseEventType.MOVE)) { if (!(events & CoreMouseEventType.MOVE)) {
@@ -537,3 +579,64 @@ export class MouseService implements IMouseService {
} }
} }
/**
* Toggles MouseEventCssClasses.ENABLE_MOUSE_EVENTS on the terminal element while alt is held when
* `mouseEventsRequireAlt` is active. DOM listeners are only registered while active.
*/
export class AltMouseCursorController implements IDisposable {
private readonly _listeners = new MutableDisposable<IDisposable>();
constructor(
private readonly _element: HTMLElement,
private readonly _document: Document,
private readonly _isActive: () => boolean
) {
}
public dispose(): void {
this._listeners.dispose();
}
public sync(): void {
this._listeners.clear();
if (!this._isActive()) {
return;
}
const store = new DisposableStore();
const syncFromModifier = (ev: KeyboardEvent | MouseEvent): void => this.syncFromModifier(ev);
store.add(addDisposableListener(this._document, 'keydown', syncFromModifier));
store.add(addDisposableListener(this._document, 'keyup', syncFromModifier));
store.add(addDisposableListener(this._element, 'mousemove', syncFromModifier));
const targetWindow = this._element.ownerDocument?.defaultView;
if (targetWindow) {
store.add(addDisposableListener(targetWindow, 'blur', () => {
if (this._isActive()) {
this.resetClass();
}
}));
}
this._listeners.value = store;
}
public resetClass(): void {
this._updateClass(false);
}
public syncFromModifier(ev: KeyboardEvent | MouseEvent): void {
if (!this._isActive()) {
return;
}
this._updateClass(ev.getModifierState('Alt'));
}
private _updateClass(altHeld: boolean): void {
if (altHeld) {
this._element.classList.add(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
} else {
this._element.classList.remove(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
}
}
}
+23 -4
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { SelectionService, SelectionMode } from './SelectionService'; import { SelectionService, SelectionMode } from './SelectionService';
import { SelectionModel } from 'browser/selection/SelectionModel'; import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferLine } from 'common/Types'; import { IBufferLine } from 'common/Types';
import { MockBufferService, MockOptionsService, MockCoreService, createCellData } from 'common/TestUtils.test'; import { MockBufferService, MockOptionsService, MockCoreService, MockMouseStateService, createCellData } from 'common/TestUtils.test';
import { BufferLine } from 'common/buffer/BufferLine'; import { BufferLine } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache'; import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services';
@@ -22,9 +22,10 @@ class TestSelectionService extends SelectionService {
constructor( constructor(
bufferService: IBufferService, bufferService: IBufferService,
optionsService: IOptionsService, optionsService: IOptionsService,
renderService: IRenderService renderService: IRenderService,
public readonly mouseStateService: MockMouseStateService
) { ) {
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService, new MockCoreBrowserService()); super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, mouseStateService, renderService, new MockCoreBrowserService());
} }
public get model(): SelectionModel { return this._model; } public get model(): SelectionModel { return this._model; }
@@ -45,16 +46,18 @@ describe('SelectionService', () => {
let buffer: IBuffer; let buffer: IBuffer;
let bufferService: IBufferService; let bufferService: IBufferService;
let optionsService: IOptionsService; let optionsService: IOptionsService;
let mouseStateService: MockMouseStateService;
let selectionService: TestSelectionService; let selectionService: TestSelectionService;
beforeEach(() => { beforeEach(() => {
optionsService = new MockOptionsService(); optionsService = new MockOptionsService();
mouseStateService = new MockMouseStateService();
bufferService = new MockBufferService(20, 20, optionsService); bufferService = new MockBufferService(20, 20, optionsService);
buffer = bufferService.buffer; buffer = bufferService.buffer;
const renderService = new MockRenderService(); const renderService = new MockRenderService();
renderService.dimensions.css.canvas.height = 10 * 20; renderService.dimensions.css.canvas.height = 10 * 20;
renderService.dimensions.css.canvas.width = 10 * 20; renderService.dimensions.css.canvas.width = 10 * 20;
selectionService = new TestSelectionService(bufferService, optionsService, renderService); selectionService = new TestSelectionService(bufferService, optionsService, renderService, mouseStateService);
}); });
function stringToRow(text: string): IBufferLine { function stringToRow(text: string): IBufferLine {
@@ -499,5 +502,21 @@ describe('SelectionService', () => {
assert.isFalse(selectionService.areCoordsInSelection([2, 1], [2, 0], [2, 1])); assert.isFalse(selectionService.areCoordsInSelection([2, 1], [2, 0], [2, 1]));
}); });
}); });
describe('shouldForceSelection', () => {
it('should force selection without alt when mouseEventsRequireAlt is enabled', () => {
optionsService.options.mouseEventsRequireAlt = true;
mouseStateService.areMouseEventsActive = true;
assert.isTrue(selectionService.shouldForceSelection({ altKey: false } as MouseEvent));
assert.isFalse(selectionService.shouldForceSelection({ altKey: true } as MouseEvent));
});
it('should take precedence over macOptionClickForcesSelection', () => {
optionsService.options.mouseEventsRequireAlt = true;
optionsService.options.macOptionClickForcesSelection = true;
mouseStateService.areMouseEventsActive = true;
assert.isFalse(selectionService.shouldForceSelection({ altKey: true } as MouseEvent));
});
});
}); });
+17 -5
View File
@@ -15,7 +15,7 @@ import { IBufferLine, ICellData, IDisposable } from 'common/Types';
import { getRangeLength } from 'common/buffer/BufferRange'; import { getRangeLength } from 'common/buffer/BufferRange';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/buffer/CellData';
import { IBuffer } from 'common/buffer/Types'; import { IBuffer } from 'common/buffer/Types';
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services'; import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event'; import { Emitter } from 'common/Event';
const enum Constants { const enum Constants {
@@ -127,6 +127,7 @@ export class SelectionService extends Disposable implements ISelectionService {
@ICoreService private readonly _coreService: ICoreService, @ICoreService private readonly _coreService: ICoreService,
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService, @IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
@IOptionsService private readonly _optionsService: IOptionsService, @IOptionsService private readonly _optionsService: IOptionsService,
@IMouseStateService private readonly _mouseStateService: IMouseStateService,
@IRenderService private readonly _renderService: IRenderService, @IRenderService private readonly _renderService: IRenderService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
) { ) {
@@ -434,6 +435,10 @@ export class SelectionService extends Disposable implements ISelectionService {
* @param event The mouse event. * @param event The mouse event.
*/ */
public shouldForceSelection(event: MouseEvent): boolean { public shouldForceSelection(event: MouseEvent): boolean {
if (this._optionsService.rawOptions.mouseEventsRequireAlt && this._mouseStateService.areMouseEventsActive) {
return !event.altKey;
}
if (Browser.isMac) { if (Browser.isMac) {
return event.altKey && this._optionsService.rawOptions.macOptionClickForcesSelection; return event.altKey && this._optionsService.rawOptions.macOptionClickForcesSelection;
} }
@@ -458,6 +463,10 @@ export class SelectionService extends Disposable implements ISelectionService {
return; return;
} }
if (this._optionsService.rawOptions.mouseEventsRequireAlt && this._mouseStateService.areMouseEventsActive && event.altKey) {
return;
}
// Allow selection when using a specific modifier key, even when disabled // Allow selection when using a specific modifier key, even when disabled
if (!this._enabled) { if (!this._enabled) {
if (!this.shouldForceSelection(event)) { if (!this.shouldForceSelection(event)) {
@@ -596,6 +605,9 @@ export class SelectionService extends Disposable implements ISelectionService {
* @param event the mouse or keyboard event * @param event the mouse or keyboard event
*/ */
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean { public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
if (this._optionsService.rawOptions.mouseEventsRequireAlt && this._mouseStateService.areMouseEventsActive) {
return false;
}
return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection); return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);
} }
@@ -918,10 +930,10 @@ export class SelectionService extends Disposable implements ISelectionService {
// Calculate the start _column_, converting the the string indexes back to // Calculate the start _column_, converting the the string indexes back to
// column coordinates. // column coordinates.
let start = let start =
startIndex // The index of the selection's start char in the line string startIndex // The index of the selection's start char in the line string
+ charOffset // The difference between the initial char's column and index + charOffset // The difference between the initial char's column and index
- leftWideCharCount // The number of wide chars left of the initial char - leftWideCharCount // The number of wide chars left of the initial char
+ leftLongCharOffset; // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis) + leftLongCharOffset; // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
// Calculate the length in _columns_, converting the the string indexes back // Calculate the length in _columns_, converting the the string indexes back
// to column coordinates. // to column coordinates.
+2 -2
View File
@@ -74,10 +74,10 @@ abstract class TaskQueue implements ITaskQueue {
private _process(deadline: ITaskDeadline): void { private _process(deadline: ITaskDeadline): void {
this._idleCallback = undefined; this._idleCallback = undefined;
let taskDuration = 0; let taskDuration: number;
let longestTask = 0; let longestTask = 0;
let lastDeadlineRemaining = deadline.timeRemaining(); let lastDeadlineRemaining = deadline.timeRemaining();
let deadlineRemaining = 0; let deadlineRemaining: number;
while (this._i < this._tasks.length) { while (this._i < this._tasks.length) {
taskDuration = performance.now(); taskDuration = performance.now();
if (!this._tasks[this._i]()) { if (!this._tasks[this._i]()) {
+3 -6
View File
@@ -98,8 +98,7 @@ CHARSETS['4'] = {
* ESC (C or ESC (5 * ESC (C or ESC (5
* Reference: http://vt100.net/docs/vt220-rm/table2-7.html * Reference: http://vt100.net/docs/vt220-rm/table2-7.html
*/ */
CHARSETS['C'] = CHARSETS['C'] = CHARSETS['5'] = {
CHARSETS['5'] = {
'[': 'Ä', '[': 'Ä',
'\\': 'Ö', '\\': 'Ö',
']': 'Å', ']': 'Å',
@@ -185,8 +184,7 @@ CHARSETS['Y'] = {
* ESC (E or ESC (6 * ESC (E or ESC (6
* Reference: http://vt100.net/docs/vt220-rm/table2-12.html * Reference: http://vt100.net/docs/vt220-rm/table2-12.html
*/ */
CHARSETS['E'] = CHARSETS['E'] = CHARSETS['6'] = {
CHARSETS['6'] = {
'@': 'Ä', '@': 'Ä',
'[': 'Æ', '[': 'Æ',
'\\': 'Ø', '\\': 'Ø',
@@ -220,8 +218,7 @@ CHARSETS['Z'] = {
* ESC (H or ESC (7 * ESC (H or ESC (7
* Reference: http://vt100.net/docs/vt220-rm/table2-14.html * Reference: http://vt100.net/docs/vt220-rm/table2-14.html
*/ */
CHARSETS['H'] = CHARSETS['H'] = CHARSETS['7'] = {
CHARSETS['7'] = {
'@': 'É', '@': 'É',
'[': 'Ä', '[': 'Ä',
'\\': 'Ö', '\\': 'Ö',
+1 -1
View File
@@ -148,7 +148,7 @@ export class Utf8ToUtf32 {
let byte2: number; let byte2: number;
let byte3: number; let byte3: number;
let byte4: number; let byte4: number;
let codepoint = 0; let codepoint;
let startPos = 0; let startPos = 0;
// handle leftover bytes // handle leftover bytes
+2 -2
View File
@@ -572,8 +572,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
* ``` * ```
*/ */
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> { public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
let code = 0; let code: number;
let transition = 0; let transition: number;
let start = 0; let start = 0;
let handlerResult: void | boolean | Promise<boolean>; let handlerResult: void | boolean | Promise<boolean>;
+3 -5
View File
@@ -88,12 +88,10 @@ export class DecorationService extends Disposable implements IDecorationService
if (!bucket) { if (!bucket) {
return; return;
} }
let xmin = 0;
let xmax = 0;
for (const d of bucket) { for (const d of bucket) {
xmin = d.options.x ?? 0; $xmin = d.options.x ?? 0;
xmax = xmin + (d.options.width ?? 1); $xmax = $xmin + (d.options.width ?? 1);
if (x >= xmin && x < xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) { if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
yield d; yield d;
} }
} }
+1
View File
@@ -41,6 +41,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
macOptionIsMeta: false, macOptionIsMeta: false,
macOptionClickForcesSelection: false, macOptionClickForcesSelection: false,
minimumContrastRatio: 1, minimumContrastRatio: 1,
mouseEventsRequireAlt: false,
disableStdin: false, disableStdin: false,
allowProposedApi: false, allowProposedApi: false,
allowTransparency: false, allowTransparency: false,
+1
View File
@@ -234,6 +234,7 @@ export interface ITerminalOptions {
macOptionIsMeta?: boolean; macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean; macOptionClickForcesSelection?: boolean;
minimumContrastRatio?: number; minimumContrastRatio?: number;
mouseEventsRequireAlt?: boolean;
reflowCursorLine?: boolean; reflowCursorLine?: boolean;
rescaleOverlappingGlyphs?: boolean; rescaleOverlappingGlyphs?: boolean;
rightClickSelectsWord?: boolean; rightClickSelectsWord?: boolean;
+1 -2
View File
@@ -4,7 +4,6 @@
"composite": true, "composite": true,
"strict": true, "strict": true,
"declarationMap": true, "declarationMap": true,
"experimentalDecorators": true, "experimentalDecorators": true
"downlevelIteration": true
} }
} }
+2 -2
View File
@@ -8,10 +8,10 @@
"types": [ "types": [
"../../node_modules/@types/node" "../../node_modules/@types/node"
], ],
"moduleResolution": "node", "moduleResolution": "nodenext",
"strict": false, "strict": false,
"target": "es2021", "target": "es2021",
"module": "commonjs", "module": "nodenext",
"paths": { "paths": {
"common/*": [ "../../src/common/*" ], "common/*": [ "../../src/common/*" ],
"browser/*": [ "../../src/browser/*" ], "browser/*": [ "../../src/browser/*" ],

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