Compare commits
262
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c28e27062 | ||
|
|
30c97a225c | ||
|
|
1186d09e05 | ||
|
|
d99e6bb2bc | ||
|
|
579a0a530f | ||
|
|
0b6220e5ea | ||
|
|
0d119b30d6 | ||
|
|
79c94de968 | ||
|
|
e3980a27db | ||
|
|
898c0d91c3 | ||
|
|
83401e3769 | ||
|
|
6b756318f0 | ||
|
|
3a9bfa94bc | ||
|
|
209e5aa6b9 | ||
|
|
24e2eb9d7b | ||
|
|
d5ae437dda | ||
|
|
5efff43e93 | ||
|
|
ee85a1da9e | ||
|
|
c30fedff6b | ||
|
|
21f51564d1 | ||
|
|
27b01b22ef | ||
|
|
387ec93a90 | ||
|
|
4388a4ac13 | ||
|
|
08c97d12b0 | ||
|
|
fbf1f7bc5d | ||
|
|
c8704930e3 | ||
|
|
e60c96823d | ||
|
|
74642ce5d5 | ||
|
|
eccbc60aa0 | ||
|
|
6b64660934 | ||
|
|
10182cc656 | ||
|
|
3c7f313c34 | ||
|
|
457542b7b3 | ||
|
|
36118d9d4c | ||
|
|
cf8a4e466d | ||
|
|
4de03aaa38 | ||
|
|
677e5275f9 | ||
|
|
d4c3b693bb | ||
|
|
2f66b5fdb6 | ||
|
|
008aa1bfb5 | ||
|
|
1d0cf32b28 | ||
|
|
c01e4b7be7 | ||
|
|
fafb195f0f | ||
|
|
2c4997f529 | ||
|
|
f0d291f645 | ||
|
|
f6b98291cf | ||
|
|
16ea2ec0ef | ||
|
|
f7118b19c6 | ||
|
|
a0574f4e9c | ||
|
|
d9b5d150ae | ||
|
|
a25db1c76d | ||
|
|
c6a572fe21 | ||
|
|
b9fa53169f | ||
|
|
eb0f5383c3 | ||
|
|
a4502a6d00 | ||
|
|
c11a8f43f4 | ||
|
|
d7ba56cd28 | ||
|
|
be44833f12 | ||
|
|
fcb3cc1a21 | ||
|
|
376268157c | ||
|
|
5ceb02b9c6 | ||
|
|
d320898231 | ||
|
|
cf3939af4a | ||
|
|
e14746e6e9 | ||
|
|
82333b9414 | ||
|
|
9eeb2799c1 | ||
|
|
5630471480 | ||
|
|
e0c59e68b2 | ||
|
|
cc8e680790 | ||
|
|
452ee50c81 | ||
|
|
f41e53a0f0 | ||
|
|
596ab845d0 | ||
|
|
a59d527d1a | ||
|
|
b287397c4f | ||
|
|
aebbe81252 | ||
|
|
d155bb6767 | ||
|
|
3a147e8e10 | ||
|
|
c2db7d4c8a | ||
|
|
456bc73bae | ||
|
|
2e776c0820 | ||
|
|
08f85cf826 | ||
|
|
6f163beb97 | ||
|
|
b0d0bf8187 | ||
|
|
32e77c3b81 | ||
|
|
e95ec8d4bc | ||
|
|
cba6e3d191 | ||
|
|
d15b426bd4 | ||
|
|
c907edd75f | ||
|
|
5741e450e6 | ||
|
|
6ef653e589 | ||
|
|
a4c0e4690e | ||
|
|
a31f78bc8b | ||
|
|
ca8e8546b5 | ||
|
|
3d473b52b6 | ||
|
|
a2bec2aeab | ||
|
|
205cf6089d | ||
|
|
8b0a6f9785 | ||
|
|
db0226be5c | ||
|
|
ab5c06cf17 | ||
|
|
9a778f0c98 | ||
|
|
c102b4af50 | ||
|
|
f3a7cac49f | ||
|
|
0d2a58c9d0 | ||
|
|
8a00898533 | ||
|
|
edb0e1ceb3 | ||
|
|
93c1650446 | ||
|
|
1029e19b57 | ||
|
|
d44900faa2 | ||
|
|
5f724c5924 | ||
|
|
77695cb405 | ||
|
|
dc4ebfd6e3 | ||
|
|
fae6f03f90 | ||
|
|
d36af624cf | ||
|
|
a105d3e457 | ||
|
|
d5900c7eae | ||
|
|
beebe8b5dc | ||
|
|
ae58176bcb | ||
|
|
429b2882f1 | ||
|
|
ffac8c03ab | ||
|
|
a7c4a160a8 | ||
|
|
c9dc994090 | ||
|
|
e4710d3286 | ||
|
|
8f9045ed88 | ||
|
|
b8e5ec7304 | ||
|
|
6a3d2480e0 | ||
|
|
735afde77f | ||
|
|
53efc95170 | ||
|
|
3512f140de | ||
|
|
dbe35428cb | ||
|
|
297ac838a3 | ||
|
|
5316cb0812 | ||
|
|
c124550971 | ||
|
|
6525a7e68e | ||
|
|
f666135881 | ||
|
|
b2133d93f0 | ||
|
|
a5e4f90139 | ||
|
|
a27b8feb8f | ||
|
|
eea03cf0a9 | ||
|
|
e96748cc79 | ||
|
|
334c8170fe | ||
|
|
2bb9662bea | ||
|
|
7cba3c9fa1 | ||
|
|
bab7db13db | ||
|
|
1c79c5511c | ||
|
|
7b417f4585 | ||
|
|
dc487dfd6f | ||
|
|
36baf70bea | ||
|
|
87e0b9ab10 | ||
|
|
5e532c13c6 | ||
|
|
db131e0c22 | ||
|
|
51b9a713da | ||
|
|
972c069fa3 | ||
|
|
2c0ba3c058 | ||
|
|
e515647f99 | ||
|
|
7a2794a552 | ||
|
|
da2267a10f | ||
|
|
34e68b1ad4 | ||
|
|
1c42957461 | ||
|
|
77036d3f12 | ||
|
|
ccbf612ddb | ||
|
|
eed8423030 | ||
|
|
431d511c95 | ||
|
|
0ca1bd48be | ||
|
|
7ceb15bb83 | ||
|
|
5ecdc7da44 | ||
|
|
2e2270d01f | ||
|
|
042bb4bed9 | ||
|
|
7e3c41ba49 | ||
|
|
21e4301e38 | ||
|
|
3ef43aedb1 | ||
|
|
9eaf5b45dc | ||
|
|
39037ee806 | ||
|
|
e542affaca | ||
|
|
038883c41b | ||
|
|
6c3d4e4ea4 | ||
|
|
659cfddd8c | ||
|
|
c375696ee6 | ||
|
|
bc3c376e83 | ||
|
|
9fca3d8ecf | ||
|
|
8eed7b4163 | ||
|
|
c29938ee83 | ||
|
|
0e679ea6ae | ||
|
|
d0e8141f7c | ||
|
|
1d322c9e45 | ||
|
|
bad7e329df | ||
|
|
ae5e7e55c8 | ||
|
|
5bb2e46508 | ||
|
|
5938b8823d | ||
|
|
69d87c8a30 | ||
|
|
a247154650 | ||
|
|
b30d9c3533 | ||
|
|
60190c1c48 | ||
|
|
99d69d2236 | ||
|
|
de01d4608c | ||
|
|
d35481c3f9 | ||
|
|
e2d84928b7 | ||
|
|
9ffd16aa9b | ||
|
|
77fa03d11c | ||
|
|
01450f044a | ||
|
|
b04d816d8a | ||
|
|
f80dbf3d03 | ||
|
|
b58bd452af | ||
|
|
e8d99d50a6 | ||
|
|
bae314f67c | ||
|
|
cb8159c4f2 | ||
|
|
b87cba4c2a | ||
|
|
492d950b64 | ||
|
|
a1b69e5ac8 | ||
|
|
d77711936f | ||
|
|
5a79b08661 | ||
|
|
3e8fc48b33 | ||
|
|
8c9afdb364 | ||
|
|
f89524e577 | ||
|
|
a1c1702508 | ||
|
|
15c71e28c8 | ||
|
|
fd641370ec | ||
|
|
259cc67f12 | ||
|
|
b0b0868e90 | ||
|
|
d58e8597cd | ||
|
|
17d439515f | ||
|
|
ae171acb79 | ||
|
|
e2dabc77fd | ||
|
|
40342ecd1e | ||
|
|
1442f84e4e | ||
|
|
6f87ae77a7 | ||
|
|
5f25e05cd2 | ||
|
|
2af9d10171 | ||
|
|
f471c0add8 | ||
|
|
62dc0d9c51 | ||
|
|
5e95ef652d | ||
|
|
14525b334f | ||
|
|
bc1d87dade | ||
|
|
849110d300 | ||
|
|
8b6f8f3172 | ||
|
|
27825f9ed8 | ||
|
|
a507afb388 | ||
|
|
ec5b421dfb | ||
|
|
8f0e711a79 | ||
|
|
fc0dc3fb07 | ||
|
|
bea1628015 | ||
|
|
447746314e | ||
|
|
6a02d7c9f5 | ||
|
|
1ace108422 | ||
|
|
2c1441a52d | ||
|
|
304943658d | ||
|
|
8e67c0093c | ||
|
|
ee3b3626e8 | ||
|
|
b286f21f64 | ||
|
|
3d92a7e2ed | ||
|
|
b01ba7db28 | ||
|
|
1c73006e39 | ||
|
|
03b015d5d3 | ||
|
|
42d408a9a7 | ||
|
|
5ecd980a83 | ||
|
|
63ebe2df5b | ||
|
|
1b2d361b59 | ||
|
|
8e29591b9f | ||
|
|
48df6117ed | ||
|
|
14b6a71391 | ||
|
|
c24eb60bd3 | ||
|
|
2521bab72f | ||
|
|
0f1c9a47db |
@@ -22,10 +22,10 @@ npm run build && npm run esbuild # Build all TypeScript and bundle
|
||||
**Testing**:
|
||||
- Unit tests: `npm run test-unit` (Mocha)
|
||||
- Unit tests filtering to file: `npm run test-unit -- **/fileName.ts
|
||||
- Per-addon unit tests: `npm run test-unit addons/addon-image/out-esbuild/*.test.js`
|
||||
- Per-addon unit tests: `npm run test-unit -- addons/addon-image/out-esbuild/*.test.js`
|
||||
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
|
||||
- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
|
||||
- Integration tests by addon: `npm run test-integration --suite=addon-search`. Suites always follow the format `addon-<something>`
|
||||
- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-<something>`
|
||||
- Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
|
||||
|
||||
## Addon Development Pattern
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "npm run agent:setup-repo"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
applyTo: '**/*.benchmark.ts'
|
||||
---
|
||||
# Benchmark run instructions
|
||||
|
||||
- Full suite: `npm run benchmark`
|
||||
- Single benchmark file:
|
||||
- Tree: `npm run benchmark -- -t out-test/benchmark/Event.benchmark.js`
|
||||
- Run file: `npm run benchmark -- -s "out-test/benchmark/Event.benchmark.js" out-test/benchmark/Event.benchmark.js`
|
||||
- Single context/case:
|
||||
- Use `-t` to get the path, then:
|
||||
- `npm run benchmark -- -s "<path>" out-test/benchmark/Event.benchmark.js`
|
||||
|
||||
When writing instructions, use `RuntimeCase` to measure pure runtime in ms, use `ThroughputRuntimeCase` when measuring throughput in MB/s.
|
||||
|
||||
Notes:
|
||||
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
|
||||
- Keep `NODE_PATH=./out` (handled by the npm script).
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -12,7 +12,7 @@ interface IAttachOptions {
|
||||
bidirectional?: boolean;
|
||||
}
|
||||
|
||||
export class AttachAddon implements ITerminalAddon , IAttachApi {
|
||||
export class AttachAddon implements ITerminalAddon, IAttachApi {
|
||||
private _socket: WebSocket;
|
||||
private _bidirectional: boolean;
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -33,7 +33,7 @@ function _getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
|
||||
return getWindow(el).getComputedStyle(el, null);
|
||||
}
|
||||
|
||||
export class FitAddon implements ITerminalAddon , IFitApi {
|
||||
export class FitAddon implements ITerminalAddon, IFitApi {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -69,9 +69,10 @@ export class FitAddon implements ITerminalAddon , IFitApi {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scrollbarWidth = (this._terminal.options.scrollback === 0
|
||||
const showScrollbar = this._terminal.options.scrollbar?.showScrollbar ?? true;
|
||||
const scrollbarWidth = (this._terminal.options.scrollback === 0 || !showScrollbar
|
||||
? 0
|
||||
: (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
|
||||
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
|
||||
|
||||
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -30,7 +30,9 @@ const customSettings: IImageAddonOptions = {
|
||||
storageLimit: 128, // FIFO storage limit in MB
|
||||
showPlaceholder: true, // whether to show a placeholder for evicted images
|
||||
iipSupport: true, // enable iTerm IIP support
|
||||
iipSizeLimit: 20000000 // size limit of a single IIP sequence
|
||||
iipSizeLimit: 20000000, // size limit of a single IIP sequence
|
||||
kittySupport: true, // enable Kitty graphics support
|
||||
kittySizeLimit: 20000000 // size limit of a single Kitty sequence
|
||||
}
|
||||
|
||||
// initialization
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 123 B |
Binary file not shown.
|
After Width: | Height: | Size: 449 B |
Binary file not shown.
|
After Width: | Height: | Size: 131 B |
@@ -18,13 +18,13 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"prepackage": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "../../node_modules/.bin/tsgo -p .",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.1.0"
|
||||
"xterm-wasm-parts": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
*/
|
||||
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
||||
|
||||
|
||||
// eslint-disable-next-line
|
||||
declare const Buffer: any;
|
||||
|
||||
// limit hold memory in base64 decoder
|
||||
const KEEP_DATA = 4194304;
|
||||
// Local const enum mirror - esbuild can't inline const enums from external packages
|
||||
const enum DecoderConst {
|
||||
// Limit held memory in base64 decoder (encoded bytes).
|
||||
KEEP_DATA = 4194304,
|
||||
// Initial buffer allocation for the decoder.
|
||||
INITIAL_DATA = 1048576,
|
||||
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
||||
OK = 0
|
||||
}
|
||||
|
||||
// default IIP header values
|
||||
const DEFAULT_HEADER: IHeaderFields = {
|
||||
@@ -31,15 +35,19 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private _aborted = false;
|
||||
private _hp = new HeaderParser();
|
||||
private _header: IHeaderFields = DEFAULT_HEADER;
|
||||
private _dec = new Base64Decoder(KEEP_DATA);
|
||||
private _dec: Base64Decoder;
|
||||
private _metrics = UNSUPPORTED_TYPE;
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _storage: IIPImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {}
|
||||
) {
|
||||
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
|
||||
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
|
||||
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
|
||||
}
|
||||
|
||||
public reset(): void {}
|
||||
|
||||
@@ -54,7 +62,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
if (this._aborted) return;
|
||||
|
||||
if (this._hp.state === HeaderState.END) {
|
||||
if (this._dec.put(data, start, end)) {
|
||||
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
@@ -70,8 +78,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._dec.init(this._header.size);
|
||||
if (this._dec.put(data, dataPos, end)) {
|
||||
this._dec.init();
|
||||
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
@@ -89,13 +97,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
let cond: number | boolean = true;
|
||||
if (cond = success) {
|
||||
if (cond = !this._dec.end()) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._metrics.height;
|
||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
||||
[w, h] = this._resize(w, h).map(Math.floor);
|
||||
cond = w && h && w * h < this._opts.pixelLimit;
|
||||
if (cond = this._dec.data8.length === this._header.size) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._metrics.height;
|
||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
||||
[w, h] = this._resize(w, h).map(Math.floor);
|
||||
cond = w && h && w * h < this._opts.pixelLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +115,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
const blob = new Blob([new Uint8Array(this._dec.data8)], { type: this._metrics.mime });
|
||||
// HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it
|
||||
const blob = new Blob([this._dec.data8 as Uint8Array<ArrayBuffer>], { type: this._metrics.mime });
|
||||
this._dec.release();
|
||||
|
||||
if (!window.createImageBitmap) {
|
||||
|
||||
@@ -8,6 +8,7 @@ declare const Buffer: any;
|
||||
|
||||
|
||||
export interface IHeaderFields {
|
||||
[key: string]: number | string | Uint32Array | null | undefined;
|
||||
// base-64 encoded filename. Defaults to "Unnamed file".
|
||||
name: string;
|
||||
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
||||
@@ -81,7 +82,7 @@ function toName(data: Uint32Array): string {
|
||||
return new TextDecoder().decode(b);
|
||||
}
|
||||
|
||||
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
|
||||
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
|
||||
inline: toInt,
|
||||
size: toInt,
|
||||
name: toName,
|
||||
@@ -100,7 +101,7 @@ export class HeaderParser {
|
||||
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
||||
private _position = 0;
|
||||
private _key = '';
|
||||
public fields: {[key: string]: any} = {};
|
||||
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
|
||||
|
||||
public reset(): void {
|
||||
this._buffer.fill(0);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ImageStorage } from './ImageStorage';
|
||||
|
||||
/**
|
||||
* IIP (iTerm Image Protocol) specific image storage controller.
|
||||
*
|
||||
* Wraps the shared ImageStorage with IIP protocol semantics:
|
||||
* - Always uses scrolling mode (cursor advances with image)
|
||||
*/
|
||||
export class IIPImageStorage {
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add an IIP image to storage.
|
||||
* Always uses scrolling mode — cursor advances past the image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
this._storage.addImage(img, true);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,11 @@ import type { ImageAddon as IImageApi } from '@xterm/addon-image';
|
||||
import { IIPHandler } from './IIPHandler';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
|
||||
import { KittyImageStorage } from './kitty/KittyImageStorage';
|
||||
import { SixelHandler } from './SixelHandler';
|
||||
import { SixelImageStorage } from './SixelImageStorage';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
||||
|
||||
// default values of addon ctor options
|
||||
@@ -22,7 +26,9 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000
|
||||
iipSizeLimit: 20000000,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 20000000
|
||||
};
|
||||
|
||||
// max palette size supported by the sixel lib (compile time setting)
|
||||
@@ -48,7 +54,7 @@ const enum GaStatus {
|
||||
}
|
||||
|
||||
|
||||
export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||
export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
private _opts: IImageAddonOptions;
|
||||
private _defaultOpts: IImageAddonOptions;
|
||||
private _storage: ImageStorage | undefined;
|
||||
@@ -90,7 +96,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||
// windowOptions.getCellSizePixels = true;
|
||||
// windowOptions.getWinSizeChars = true;
|
||||
// terminal.setOption('windowOptions', windowOptions);
|
||||
const windowOps = terminal.options.windowOptions || {};
|
||||
const windowOps = terminal.options.windowOptions ?? {};
|
||||
windowOps.getWinSizePixels = true;
|
||||
windowOps.getCellSizePixels = true;
|
||||
windowOps.getWinSizeChars = true;
|
||||
@@ -129,7 +135,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||
|
||||
// SIXEL handler
|
||||
if (this._opts.sixelSupport) {
|
||||
const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
|
||||
const sixelStorage = new SixelImageStorage(this._storage!, this._opts, this._renderer!, terminal);
|
||||
const sixelHandler = new SixelHandler(this._opts, sixelStorage, terminal);
|
||||
this._handlers.set('sixel', sixelHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
|
||||
@@ -138,12 +145,25 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||
|
||||
// iTerm IIP handler
|
||||
if (this._opts.iipSupport) {
|
||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
|
||||
const iipStorage = new IIPImageStorage(this._storage!);
|
||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
|
||||
this._handlers.set('iip', iipHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
||||
);
|
||||
}
|
||||
|
||||
// Kitty graphics handler
|
||||
if (this._opts.kittySupport) {
|
||||
const kittyStorage = new KittyImageStorage(this._storage!);
|
||||
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
|
||||
this._handlers.set('kitty', kittyHandler);
|
||||
this._disposeLater(
|
||||
kittyStorage,
|
||||
kittyHandler,
|
||||
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import { toRGBA8888 } from 'sixel/lib/Colors';
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
const PLACEHOLDER_LENGTH = 4096;
|
||||
const PLACEHOLDER_HEIGHT = 24;
|
||||
@@ -18,8 +18,9 @@ const PLACEHOLDER_HEIGHT = 24;
|
||||
* - draw image tiles onRender
|
||||
*/
|
||||
export class ImageRenderer extends Disposable implements IDisposable {
|
||||
public canvas: HTMLCanvasElement | undefined;
|
||||
private _ctx: CanvasRenderingContext2D | null | undefined;
|
||||
/** @deprecated Kept for backward compat — points to top layer canvas. */
|
||||
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
|
||||
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
|
||||
private _placeholder: HTMLCanvasElement | undefined;
|
||||
private _placeholderBitmap: ImageBitmap | undefined;
|
||||
private _optionsRefresh = this._register(new MutableDisposable());
|
||||
@@ -38,7 +39,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
* Only the DOM output canvas should be on the terminal's document,
|
||||
* which gets explicitly checked in `insertLayerToDom`.
|
||||
*/
|
||||
const canvas = (localDocument || document).createElement('canvas');
|
||||
const canvas = (localDocument ?? document).createElement('canvas');
|
||||
canvas.width = width | 0;
|
||||
canvas.height = height | 0;
|
||||
return canvas;
|
||||
@@ -86,6 +87,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
});
|
||||
this._register(toDisposable(() => {
|
||||
this.removeLayerFromDom();
|
||||
this.removeLayerFromDom('bottom');
|
||||
if (this._terminal._core && this._oldOpen) {
|
||||
this._terminal._core.open = this._oldOpen;
|
||||
this._oldOpen = undefined;
|
||||
@@ -95,8 +97,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
this._oldSetRenderer = undefined;
|
||||
}
|
||||
this._renderService = undefined;
|
||||
this.canvas = undefined;
|
||||
this._ctx = undefined;
|
||||
this._layers.clear();
|
||||
this._placeholderBitmap?.close();
|
||||
this._placeholderBitmap = undefined;
|
||||
this._placeholder = undefined;
|
||||
@@ -140,27 +141,38 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
/**
|
||||
* Clear a region of the image layer canvas.
|
||||
*/
|
||||
public clearLines(start: number, end: number): void {
|
||||
this._ctx?.clearRect(
|
||||
0,
|
||||
start * (this.dimensions?.css.cell.height || 0),
|
||||
this.dimensions?.css.canvas.width || 0,
|
||||
(++end - start) * (this.dimensions?.css.cell.height || 0)
|
||||
);
|
||||
public clearLines(start: number, end: number, layer?: ImageLayer): void {
|
||||
const y = start * (this.dimensions?.css.cell.height || 0);
|
||||
const w = this.dimensions?.css.canvas.width || 0;
|
||||
const h = (++end - start) * (this.dimensions?.css.cell.height || 0);
|
||||
if (!layer || layer === 'top') {
|
||||
this._layers.get('top')?.clearRect(0, y, w, h);
|
||||
}
|
||||
if (!layer || layer === 'bottom') {
|
||||
this._layers.get('bottom')?.clearRect(0, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear whole image canvas.
|
||||
*/
|
||||
public clearAll(): void {
|
||||
this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
|
||||
public clearAll(layer?: ImageLayer): void {
|
||||
if (!layer || layer === 'top') {
|
||||
const ctx = this._layers.get('top');
|
||||
ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
}
|
||||
if (!layer || layer === 'bottom') {
|
||||
const ctx = this._layers.get('bottom');
|
||||
ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw neighboring tiles on the image layer canvas.
|
||||
*/
|
||||
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
||||
if (!this._ctx) {
|
||||
const ctx = this._layers.get(imgSpec.layer);
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
const { width, height } = this.cellSize;
|
||||
@@ -187,7 +199,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
||||
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
||||
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
||||
this._ctx.drawImage(
|
||||
ctx.drawImage(
|
||||
img,
|
||||
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
||||
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
||||
@@ -227,7 +239,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
* Draw a line with placeholder on the image layer canvas.
|
||||
*/
|
||||
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
||||
if (this._ctx) {
|
||||
const ctx = this._layers.get('top');
|
||||
if (ctx) {
|
||||
const { width, height } = this.cellSize;
|
||||
|
||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
||||
@@ -241,8 +254,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
this._createPlaceHolder(height + 1);
|
||||
}
|
||||
if (!this._placeholder) return;
|
||||
this._ctx.drawImage(
|
||||
this._placeholderBitmap || this._placeholder!,
|
||||
ctx.drawImage(
|
||||
this._placeholderBitmap ?? this._placeholder!,
|
||||
col * width,
|
||||
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
||||
width * count,
|
||||
@@ -260,12 +273,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
* Checked once from `ImageStorage.render`.
|
||||
*/
|
||||
public rescaleCanvas(): void {
|
||||
if (!this.canvas) {
|
||||
return;
|
||||
}
|
||||
if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
|
||||
this.canvas.width = this.dimensions!.css.canvas.width || 0;
|
||||
this.canvas.height = this.dimensions!.css.canvas.height || 0;
|
||||
const w = this.dimensions?.css.canvas.width || 0;
|
||||
const h = this.dimensions?.css.canvas.height || 0;
|
||||
for (const ctx of this._layers.values()) {
|
||||
if (ctx.canvas.width !== w || ctx.canvas.height !== h) {
|
||||
ctx.canvas.width = w;
|
||||
ctx.canvas.height = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,35 +318,65 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
this._renderService = this._terminal._core._renderService;
|
||||
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
|
||||
this._renderService.setRenderer = (renderer: any) => {
|
||||
this.removeLayerFromDom();
|
||||
for (const key of [...this._layers.keys()]) {
|
||||
this.removeLayerFromDom(key);
|
||||
}
|
||||
this._oldSetRenderer?.call(this._renderService, renderer);
|
||||
};
|
||||
}
|
||||
|
||||
public insertLayerToDom(): void {
|
||||
public insertLayerToDom(layer: ImageLayer = 'top'): void {
|
||||
// make sure that the terminal is attached to a document and to DOM
|
||||
if (this.document && this._terminal._core.screenElement) {
|
||||
if (!this.canvas) {
|
||||
this.canvas = ImageRenderer.createCanvas(
|
||||
this.document, this.dimensions?.css.canvas.width || 0,
|
||||
this.dimensions?.css.canvas.height || 0
|
||||
);
|
||||
this.canvas.classList.add('xterm-image-layer');
|
||||
this._terminal._core.screenElement.appendChild(this.canvas);
|
||||
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
|
||||
this.clearAll();
|
||||
}
|
||||
} else {
|
||||
if (!this.document || !this._terminal._core.screenElement) {
|
||||
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
|
||||
return;
|
||||
}
|
||||
if (this._layers.has(layer)) {
|
||||
return;
|
||||
}
|
||||
const canvas = ImageRenderer.createCanvas(
|
||||
this.document, this.dimensions?.css.canvas.width || 0,
|
||||
this.dimensions?.css.canvas.height || 0
|
||||
);
|
||||
canvas.classList.add(`xterm-image-layer-${layer}`);
|
||||
const screenElement = this._terminal._core.screenElement;
|
||||
// Use isolation to create a stacking context without overriding z-index,
|
||||
// which would conflict with integrators (e.g. VS Code) that set their
|
||||
// own z-index on the screen element.
|
||||
screenElement.style.isolation = 'isolate';
|
||||
if (layer === 'bottom') {
|
||||
// Use z-index:-1 so it paints behind non-positioned text elements.
|
||||
// The screen element needs to be a stacking context (via isolation)
|
||||
// to contain the negative z-index, otherwise it would go behind the
|
||||
// entire terminal.
|
||||
canvas.style.zIndex = '-1';
|
||||
screenElement.insertBefore(canvas, screenElement.firstChild);
|
||||
} else {
|
||||
// Explicit z-index ensures the image canvas reliably stacks above
|
||||
// the text layer (DOM renderer rows). z-index: 0 is below the
|
||||
// selection overlay (z-index: 1).
|
||||
canvas.style.zIndex = '0';
|
||||
screenElement.appendChild(canvas);
|
||||
}
|
||||
const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });
|
||||
if (!ctx) {
|
||||
canvas.remove();
|
||||
return;
|
||||
}
|
||||
this._layers.set(layer, ctx);
|
||||
this.clearAll(layer);
|
||||
}
|
||||
|
||||
public removeLayerFromDom(layer: ImageLayer = 'top'): void {
|
||||
const ctx = this._layers.get(layer);
|
||||
if (ctx) {
|
||||
ctx.canvas.remove();
|
||||
this._layers.delete(layer);
|
||||
}
|
||||
}
|
||||
|
||||
public removeLayerFromDom(): void {
|
||||
if (this.canvas) {
|
||||
this._ctx = undefined;
|
||||
this.canvas.remove();
|
||||
this.canvas = undefined;
|
||||
}
|
||||
public hasLayer(layer: ImageLayer): boolean {
|
||||
return this._layers.has(layer);
|
||||
}
|
||||
|
||||
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
|
||||
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types';
|
||||
|
||||
|
||||
// fallback default cell size
|
||||
@@ -124,6 +124,7 @@ export class ImageStorage implements IDisposable {
|
||||
private _pixelLimit: number = 2500000;
|
||||
|
||||
private _viewportMetrics: { cols: number, rows: number };
|
||||
public onImageDeleted: ((storageId: number) => void) | undefined;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminalExt,
|
||||
@@ -132,8 +133,10 @@ export class ImageStorage implements IDisposable {
|
||||
) {
|
||||
try {
|
||||
this.setLimit(this._opts.storageLimit);
|
||||
} catch (e: any) {
|
||||
console.error(e.message);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
console.error(e.message);
|
||||
}
|
||||
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
|
||||
}
|
||||
this._viewportMetrics = {
|
||||
@@ -187,11 +190,13 @@ export class ImageStorage implements IDisposable {
|
||||
|
||||
private _delImg(id: number): void {
|
||||
const spec = this._images.get(id);
|
||||
if (!spec) return;
|
||||
this._images.delete(id);
|
||||
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
||||
if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
||||
if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
||||
spec.orig.close();
|
||||
}
|
||||
this.onImageDeleted?.(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,27 +220,27 @@ export class ImageStorage implements IDisposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Only advance text cursor.
|
||||
* This is an edge case from empty sixels carrying only a height but no pixels.
|
||||
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
||||
* Delete an image by its internal storage ID.
|
||||
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
|
||||
*/
|
||||
public advanceCursor(height: number): void {
|
||||
if (this._opts.sixelScrolling) {
|
||||
let cellSize = this._renderer.cellSize;
|
||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
||||
cellSize = CELL_SIZE_DEFAULT;
|
||||
}
|
||||
const rows = Math.ceil(height / cellSize.height);
|
||||
for (let i = 1; i < rows; ++i) {
|
||||
this._terminal._core._inputHandler.lineFeed();
|
||||
}
|
||||
public deleteImage(id: number): void {
|
||||
const spec = this._images.get(id);
|
||||
if (spec) {
|
||||
spec.marker?.dispose();
|
||||
this._delImg(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add an image to the storage.
|
||||
* @param img - The image to add (canvas or bitmap).
|
||||
* @param scrolling - When true, cursor advances with the image (lineFeed per row).
|
||||
* When false, image is placed at (0,0) and cursor is restored (DECSET 80 / sixel origin mode).
|
||||
* @param layer - Which canvas layer to render on ('top' or 'bottom').
|
||||
* @param zIndex - Z-index for image layering within the same layer.
|
||||
* @returns The internal image ID assigned to the stored image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer = 'top', zIndex: number = 0): number {
|
||||
// never allow storage to exceed memory limit
|
||||
this._evictOldest(img.width * img.height);
|
||||
|
||||
@@ -257,7 +262,7 @@ export class ImageStorage implements IDisposable {
|
||||
let offset = originX;
|
||||
let tileCount = 0;
|
||||
|
||||
if (!this._opts.sixelScrolling) {
|
||||
if (!scrolling) {
|
||||
buffer.x = 0;
|
||||
buffer.y = 0;
|
||||
offset = 0;
|
||||
@@ -271,7 +276,7 @@ export class ImageStorage implements IDisposable {
|
||||
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
|
||||
tileCount++;
|
||||
}
|
||||
if (this._opts.sixelScrolling) {
|
||||
if (scrolling) {
|
||||
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
|
||||
} else {
|
||||
if (++buffer.y >= termRows) break;
|
||||
@@ -281,7 +286,7 @@ export class ImageStorage implements IDisposable {
|
||||
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
||||
|
||||
// cursor positioning modes
|
||||
if (this._opts.sixelScrolling) {
|
||||
if (scrolling) {
|
||||
buffer.x = offset;
|
||||
} else {
|
||||
buffer.x = originX;
|
||||
@@ -324,11 +329,14 @@ export class ImageStorage implements IDisposable {
|
||||
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
||||
marker: endMarker || undefined,
|
||||
tileCount,
|
||||
bufferType: this._terminal.buffer.active.type
|
||||
bufferType: this._terminal.buffer.active.type,
|
||||
layer,
|
||||
zIndex
|
||||
};
|
||||
|
||||
// finally add the image
|
||||
this._images.set(imageId, imgSpec);
|
||||
return imageId;
|
||||
}
|
||||
|
||||
|
||||
@@ -338,16 +346,30 @@ export class ImageStorage implements IDisposable {
|
||||
*/
|
||||
// TODO: Should we move this to the ImageRenderer?
|
||||
public render(range: { start: number, end: number }): void {
|
||||
// setup image canvas in case we have none yet, but have images in store
|
||||
if (!this._renderer.canvas && this._images.size) {
|
||||
this._renderer.insertLayerToDom();
|
||||
// safety measure - in case we cannot spawn a canvas at all, just exit
|
||||
if (!this._renderer.canvas) {
|
||||
return;
|
||||
// Determine which layers have images
|
||||
let hasTopImages = false;
|
||||
let hasBottomImages = false;
|
||||
for (const spec of this._images.values()) {
|
||||
if (spec.layer === 'bottom') {
|
||||
hasBottomImages = true;
|
||||
} else {
|
||||
hasTopImages = true;
|
||||
}
|
||||
if (hasTopImages && hasBottomImages) break;
|
||||
}
|
||||
|
||||
// Lazily insert layers that are needed
|
||||
if (hasTopImages && !this._renderer.hasLayer('top')) {
|
||||
this._renderer.insertLayerToDom('top');
|
||||
if (!this._renderer.hasLayer('top')) return;
|
||||
}
|
||||
if (hasBottomImages && !this._renderer.hasLayer('bottom')) {
|
||||
this._renderer.insertLayerToDom('bottom');
|
||||
}
|
||||
|
||||
// rescale if needed
|
||||
this._renderer.rescaleCanvas();
|
||||
|
||||
// exit early if we dont have any images to test for
|
||||
if (!this._images.size) {
|
||||
if (!this._fullyCleared) {
|
||||
@@ -355,12 +377,25 @@ export class ImageStorage implements IDisposable {
|
||||
this._fullyCleared = true;
|
||||
this._needsFullClear = false;
|
||||
}
|
||||
if (this._renderer.canvas) {
|
||||
this._renderer.removeLayerFromDom();
|
||||
if (this._renderer.hasLayer('top')) {
|
||||
this._renderer.removeLayerFromDom('top');
|
||||
}
|
||||
if (this._renderer.hasLayer('bottom')) {
|
||||
this._renderer.removeLayerFromDom('bottom');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove layers no longer needed
|
||||
if (!hasTopImages && this._renderer.hasLayer('top')) {
|
||||
this._renderer.clearAll('top');
|
||||
this._renderer.removeLayerFromDom('top');
|
||||
}
|
||||
if (!hasBottomImages && this._renderer.hasLayer('bottom')) {
|
||||
this._renderer.clearAll('bottom');
|
||||
this._renderer.removeLayerFromDom('bottom');
|
||||
}
|
||||
|
||||
// buffer switches force a full clear
|
||||
if (this._needsFullClear) {
|
||||
this._renderer.clearAll();
|
||||
@@ -375,50 +410,77 @@ export class ImageStorage implements IDisposable {
|
||||
// clear drawing area
|
||||
this._renderer.clearLines(start, end);
|
||||
|
||||
// walk all cells in viewport and draw tiles found
|
||||
// Collect draw calls so we can sort by z-index (lower z drawn first).
|
||||
const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
|
||||
const placeholderCalls: { col: number, row: number, count: number }[] = [];
|
||||
|
||||
// walk all cells in viewport and collect tiles found
|
||||
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
|
||||
// because text writes clear the BG flag but leave image tile data intact.
|
||||
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
|
||||
for (let row = start; row <= end; ++row) {
|
||||
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
|
||||
if (!line) return;
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
let e: IExtendedAttrsImage;
|
||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
||||
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
||||
} else {
|
||||
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
||||
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
const imgSpec = this._images.get(imageId);
|
||||
if (e.tileId !== -1) {
|
||||
const startTile = e.tileId;
|
||||
const startCol = col;
|
||||
let count = 1;
|
||||
/**
|
||||
* merge tiles to the right into a single draw call, if:
|
||||
* - not at end of line
|
||||
* - cell has same image id
|
||||
* - cell has consecutive tile id
|
||||
*/
|
||||
while (
|
||||
++col < cols
|
||||
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
|
||||
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
|
||||
&& (e.imageId === imageId)
|
||||
&& (e.tileId === startTile + count)
|
||||
) {
|
||||
count++;
|
||||
e = maybeImg;
|
||||
}
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
const imgSpec = this._images.get(imageId);
|
||||
if (e.tileId !== -1) {
|
||||
const startTile = e.tileId;
|
||||
const startCol = col;
|
||||
let count = 1;
|
||||
/**
|
||||
* merge tiles to the right into a single draw call, if:
|
||||
* - not at end of line
|
||||
* - cell has same image id
|
||||
* - cell has consecutive tile id
|
||||
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
|
||||
*/
|
||||
while (++col < cols) {
|
||||
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
||||
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
||||
break;
|
||||
}
|
||||
col--;
|
||||
if (imgSpec) {
|
||||
if (imgSpec.actual) {
|
||||
this._renderer.draw(imgSpec, startTile, startCol, row, count);
|
||||
}
|
||||
} else if (this._opts.showPlaceholder) {
|
||||
this._renderer.drawPlaceholder(startCol, row, count);
|
||||
}
|
||||
this._fullyCleared = false;
|
||||
e = nextE;
|
||||
count++;
|
||||
}
|
||||
col--;
|
||||
if (imgSpec) {
|
||||
if (imgSpec.actual) {
|
||||
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
||||
}
|
||||
} else if (this._opts.showPlaceholder) {
|
||||
placeholderCalls.push({ col: startCol, row, count });
|
||||
}
|
||||
this._fullyCleared = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by z-index so lower z draws first (higher z renders on top)
|
||||
drawCalls.sort((a, b) => a.imgSpec.zIndex - b.imgSpec.zIndex);
|
||||
|
||||
// Draw placeholders first (lowest priority)
|
||||
for (const call of placeholderCalls) {
|
||||
this._renderer.drawPlaceholder(call.col, call.row, call.count);
|
||||
}
|
||||
|
||||
// Draw images in z-index order
|
||||
for (const call of drawCalls) {
|
||||
this._renderer.draw(call.imgSpec, call.tileId, call.col, call.row, call.count);
|
||||
}
|
||||
}
|
||||
|
||||
public viewportResize(metrics: { cols: number, rows: number }): void {
|
||||
@@ -442,7 +504,7 @@ export class ImageStorage implements IDisposable {
|
||||
for (let row = 0; row < rows; ++row) {
|
||||
const line = buffer.lines.get(row) as IBufferLineExt;
|
||||
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
@@ -487,7 +549,7 @@ export class ImageStorage implements IDisposable {
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
||||
if (e.imageId && e.imageId !== -1) {
|
||||
const orig = this._images.get(e.imageId)?.orig;
|
||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
||||
@@ -507,7 +569,7 @@ export class ImageStorage implements IDisposable {
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
||||
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
||||
const spec = this._images.get(e.imageId);
|
||||
if (spec) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ImageStorage } from './ImageStorage';
|
||||
import { SixelImageStorage } from './SixelImageStorage';
|
||||
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
|
||||
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
|
||||
import { RGBA8888 } from 'sixel/lib/Types';
|
||||
@@ -26,7 +26,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _storage: SixelImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {
|
||||
DecoderAsync({
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { IImageAddonOptions, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
|
||||
/**
|
||||
* Sixel-specific image storage controller.
|
||||
*
|
||||
* Wraps the shared ImageStorage with sixel protocol semantics:
|
||||
* - Cursor behavior governed by DECSET 80 (sixelScrolling option)
|
||||
* - advanceCursor for empty sixels carrying only height
|
||||
*/
|
||||
export class SixelImageStorage {
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _terminal: ITerminalExt
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add a sixel image to storage.
|
||||
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
this._storage.addImage(img, this._opts.sixelScrolling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only advance text cursor.
|
||||
* This is an edge case from empty sixels carrying only a height but no pixels.
|
||||
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
||||
*/
|
||||
public advanceCursor(height: number): void {
|
||||
if (this._opts.sixelScrolling) {
|
||||
let cellSize = this._renderer.cellSize;
|
||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
||||
cellSize = CELL_SIZE_DEFAULT;
|
||||
}
|
||||
const rows = Math.ceil(height / cellSize.height);
|
||||
for (let i = 1; i < rows; ++i) {
|
||||
this._terminal._core._inputHandler.lineFeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
|
||||
// private imports from base repo we build against
|
||||
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import type { AttributeData } from 'common/buffer/AttributeData';
|
||||
import type { IParams, IDcsHandler, IOscHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
|
||||
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
@@ -22,7 +22,7 @@ export const enum Cell {
|
||||
}
|
||||
|
||||
// export some privates for local usage
|
||||
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
||||
export { AttributeData, IParams, IDcsHandler, IOscHandler, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
||||
|
||||
/**
|
||||
* Plugin ctor options.
|
||||
@@ -38,6 +38,8 @@ export interface IImageAddonOptions {
|
||||
sixelSizeLimit: number;
|
||||
iipSupport: boolean;
|
||||
iipSizeLimit: number;
|
||||
kittySupport: boolean;
|
||||
kittySizeLimit: number;
|
||||
}
|
||||
|
||||
export interface IResetHandler {
|
||||
@@ -97,6 +99,8 @@ export interface ICellSize {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type ImageLayer = 'top' | 'bottom';
|
||||
|
||||
export interface IImageSpec {
|
||||
orig: HTMLCanvasElement | ImageBitmap | undefined;
|
||||
origCellSize: ICellSize;
|
||||
@@ -105,4 +109,6 @@ export interface IImageSpec {
|
||||
marker: IMarker | undefined;
|
||||
tileCount: number;
|
||||
bufferType: 'alternate' | 'normal';
|
||||
layer: ImageLayer;
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,813 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { IApcHandler, IImageAddonOptions, IResetHandler, ITerminalExt, ImageLayer } from '../Types';
|
||||
import { ImageRenderer } from '../ImageRenderer';
|
||||
import { CELL_SIZE_DEFAULT } from '../ImageStorage';
|
||||
import { KittyImageStorage } from './KittyImageStorage';
|
||||
import Base64Decoder, { type DecodeStatus } from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import {
|
||||
KittyAction,
|
||||
KittyFormat,
|
||||
KittyCompression,
|
||||
IKittyCommand,
|
||||
IPendingTransmission,
|
||||
IKittyImageData,
|
||||
BYTES_PER_PIXEL_RGB,
|
||||
BYTES_PER_PIXEL_RGBA,
|
||||
ALPHA_OPAQUE,
|
||||
parseKittyCommand
|
||||
} from './KittyGraphicsTypes';
|
||||
|
||||
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
||||
const DECODER_KEEP_DATA = 4194304;
|
||||
const DECODER_INITIAL_DATA = 4194304; // 4MB
|
||||
|
||||
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
||||
const DECODER_OK: DecodeStatus.OK = 0;
|
||||
|
||||
// Maximum control data size
|
||||
const MAX_CONTROL_DATA_SIZE = 512;
|
||||
|
||||
// Semicolon codepoint
|
||||
const SEMICOLON = 0x3B;
|
||||
|
||||
// Kitty graphics protocol handler with streaming base64 decoding.
|
||||
export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDisposable {
|
||||
private _aborted = false;
|
||||
private _decodeError = false;
|
||||
|
||||
private _activeDecoder: Base64Decoder | null = null;
|
||||
private readonly _maxEncodedBytes: number;
|
||||
private readonly _initialEncodedBytes: number;
|
||||
|
||||
// Streaming related states
|
||||
|
||||
// True while receiving control data (before semicolon).
|
||||
private _inControlData = true;
|
||||
|
||||
// Buffer for control data.
|
||||
private _controlData = new Uint32Array(MAX_CONTROL_DATA_SIZE);
|
||||
private _controlLength = 0;
|
||||
|
||||
// Pre-calculated encoded size limit
|
||||
private _encodedSizeLimit = 0;
|
||||
private _totalEncodedSize = 0;
|
||||
|
||||
// Parsed command. These are the control data before semicolon.
|
||||
private _parsedCommand: IKittyCommand | null = null;
|
||||
|
||||
// Storage related states
|
||||
|
||||
private _pendingTransmissions: Map<number, IPendingTransmission> = new Map();
|
||||
// Tracks the pending key of the most recently started chunked upload.
|
||||
// Per spec, subsequent chunks only need m= (and optionally q=), without i=.
|
||||
// When a chunk arrives with no i=, this key is used to find the pending upload.
|
||||
private _lastPendingKey: number | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _kittyStorage: KittyImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {
|
||||
// Convert decoded size limit -> max encoded bytes.
|
||||
this._maxEncodedBytes = Math.ceil(this._opts.kittySizeLimit * 4 / 3);
|
||||
// ensure we preallocate more than configured limit while using 4mb initial size.
|
||||
this._initialEncodedBytes = Math.min(DECODER_INITIAL_DATA, this._maxEncodedBytes);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._cleanupAllPending();
|
||||
if (this._activeDecoder) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
this._kittyStorage.reset();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private _removePendingEntry(key: number): void {
|
||||
this._pendingTransmissions.delete(key);
|
||||
if (this._lastPendingKey === key) {
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _cleanupAllPending(): void {
|
||||
for (const pending of this._pendingTransmissions.values()) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this._aborted = false;
|
||||
this._decodeError = false;
|
||||
this._inControlData = true;
|
||||
this._controlLength = 0;
|
||||
this._parsedCommand = null;
|
||||
// Pre-calculate encoded limit once: base64 is 4 bytes encoded → 3 bytes decoded
|
||||
this._encodedSizeLimit = this._maxEncodedBytes;
|
||||
this._totalEncodedSize = 0;
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._aborted) return;
|
||||
|
||||
if (!this._inControlData) {
|
||||
this._streamPayload(data, start, end);
|
||||
} else {
|
||||
// Scan for semicolon
|
||||
let controlEnd = end;
|
||||
for (let i = start; i < end; i++) {
|
||||
if (data[i] === SEMICOLON) {
|
||||
this._inControlData = false;
|
||||
controlEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy control data
|
||||
const copyLength = controlEnd - start;
|
||||
if (this._controlLength + copyLength > MAX_CONTROL_DATA_SIZE) {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._controlData.set(data.subarray(start, controlEnd), this._controlLength);
|
||||
this._controlLength += copyLength;
|
||||
|
||||
if (!this._inControlData) {
|
||||
// Found semicolon - parse control data early for validation
|
||||
this._parsedCommand = parseKittyCommand(this._parseControlDataString());
|
||||
|
||||
// Early validation: i+I conflict
|
||||
if (this._parsedCommand.id !== undefined && this._parsedCommand.imageNumber !== undefined) {
|
||||
this._sendResponse(this._parsedCommand.id, 'EINVAL:cannot specify both i and I keys', this._parsedCommand.quiet ?? 0);
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete action doesn't need payload - skip streaming
|
||||
if (this._parsedCommand.action === KittyAction.DELETE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream remaining as payload
|
||||
const payloadStart = controlEnd + 1;
|
||||
if (payloadStart < end) {
|
||||
this._streamPayload(data, payloadStart, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream payload bytes into the base64 decoder.
|
||||
private _streamPayload(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._aborted) return;
|
||||
|
||||
// Check size limit (compare encoded bytes against pre-calculated limit)
|
||||
// Include cumulative size from pending transmission for multi-chunk images.
|
||||
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
|
||||
const pendingKey = this._parsedCommand?.id ?? this._lastPendingKey ?? 0;
|
||||
const pending = this._pendingTransmissions.get(pendingKey);
|
||||
const previousEncodedSize = pending?.totalEncodedSize ?? 0;
|
||||
this._totalEncodedSize += end - start;
|
||||
const cumulativeEncodedSize = previousEncodedSize + this._totalEncodedSize;
|
||||
if (cumulativeEncodedSize > this._encodedSizeLimit) {
|
||||
const decoderToRelease = this._activeDecoder ?? pending?.decoder;
|
||||
if (decoderToRelease) {
|
||||
decoderToRelease.release();
|
||||
}
|
||||
this._activeDecoder = null;
|
||||
if (pending) {
|
||||
this._removePendingEntry(pendingKey);
|
||||
}
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._decodeError) return;
|
||||
|
||||
if (pending?.decoder && !this._activeDecoder) {
|
||||
this._activeDecoder = pending.decoder;
|
||||
}
|
||||
if (!this._activeDecoder) {
|
||||
this._activeDecoder = new Base64Decoder(DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
|
||||
this._activeDecoder.init();
|
||||
}
|
||||
|
||||
if (this._activeDecoder.put(data.subarray(start, end)) !== DECODER_OK) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
this._decodeError = true;
|
||||
if (pending) {
|
||||
this._removePendingEntry(pendingKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public end(success: boolean): boolean | Promise<boolean> {
|
||||
if (this._aborted || !success) {
|
||||
if (this._activeDecoder) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// No semicolon = no payload (delete, capability query)
|
||||
if (this._inControlData) {
|
||||
return this._handleNoPayloadCommand();
|
||||
}
|
||||
|
||||
// Use command parsed early in put() - i+I already validated there
|
||||
const cmd = this._parsedCommand!;
|
||||
|
||||
// Delete action was handled by skipping payload - just execute
|
||||
if (cmd.action === KittyAction.DELETE) {
|
||||
return this._handleDelete(cmd);
|
||||
}
|
||||
|
||||
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
|
||||
const pendingKey = cmd.id ?? this._lastPendingKey ?? 0;
|
||||
const isMoreComing = cmd.more === 1;
|
||||
const pending = this._pendingTransmissions.get(pendingKey);
|
||||
|
||||
if (isMoreComing) {
|
||||
if (this._activeDecoder) {
|
||||
if (pending) {
|
||||
pending.totalEncodedSize += this._totalEncodedSize;
|
||||
pending.decodeError = pending.decodeError || this._decodeError;
|
||||
} else {
|
||||
this._pendingTransmissions.set(pendingKey, {
|
||||
cmd: { ...cmd },
|
||||
decoder: this._activeDecoder,
|
||||
totalEncodedSize: this._totalEncodedSize,
|
||||
decodeError: this._decodeError
|
||||
});
|
||||
}
|
||||
this._lastPendingKey = pendingKey;
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Final chunk received — clear the last pending key
|
||||
if (pending) {
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
|
||||
let decodeError = this._decodeError;
|
||||
let finalCmd = cmd;
|
||||
let decoder = this._activeDecoder;
|
||||
|
||||
if (pending) {
|
||||
finalCmd = pending.cmd;
|
||||
decoder = pending.decoder;
|
||||
decodeError = decodeError || pending.decodeError;
|
||||
this._pendingTransmissions.delete(pendingKey);
|
||||
}
|
||||
|
||||
let imageBytes = new Uint8Array(0);
|
||||
if (decoder) {
|
||||
if (decoder.end() !== DECODER_OK) {
|
||||
decodeError = true;
|
||||
}
|
||||
imageBytes = decoder.data8;
|
||||
}
|
||||
this._activeDecoder = null;
|
||||
|
||||
// Handle command first — handlers create Blob/ImageData from imageBytes,
|
||||
// which copies the data. Only then is it safe to release the decoder's
|
||||
// wasm memory that imageBytes points into.
|
||||
const result = this._handleCommandWithBytesAndCmd(finalCmd, imageBytes, decodeError);
|
||||
if (decoder) {
|
||||
decoder.release();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Command handling
|
||||
|
||||
private _parseControlDataString(): string {
|
||||
let str = '';
|
||||
for (let i = 0; i < this._controlLength; i++) {
|
||||
str += String.fromCodePoint(this._controlData[i]);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
private _handleNoPayloadCommand(): boolean | Promise<boolean> {
|
||||
const cmd = parseKittyCommand(this._parseControlDataString());
|
||||
|
||||
// Per spec: specifying both i and I is an error
|
||||
if (cmd.id !== undefined && cmd.imageNumber !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:cannot specify both i and I keys', cmd.quiet ?? 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
const action = cmd.action ?? 't';
|
||||
|
||||
switch (action) {
|
||||
case KittyAction.DELETE:
|
||||
return this._handleDelete(cmd);
|
||||
case KittyAction.QUERY:
|
||||
this._sendResponse(cmd.id ?? 0, 'OK', cmd.quiet ?? 0);
|
||||
return true;
|
||||
case KittyAction.PLACEMENT:
|
||||
return this._handlePlacement(cmd);
|
||||
default:
|
||||
// TODO: Implement remaining actions when needed:
|
||||
// - a=f (frame): animation frame operations
|
||||
// - a=a (animation): animation control
|
||||
// - a=c (compose): compose images
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleCommandWithBytesAndCmd(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
|
||||
const action = cmd.action ?? 't';
|
||||
|
||||
switch (action) {
|
||||
case KittyAction.TRANSMIT: {
|
||||
const result = this._handleTransmit(cmd, bytes, decodeError);
|
||||
// Only send response when _handleTransmit didn't already respond
|
||||
// (it handles unsupported transmission medium responses internally)
|
||||
if ((cmd.transmission ?? 'd') === 'd' && cmd.id !== undefined) {
|
||||
if (decodeError) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
|
||||
} else if (bytes.length > 0) {
|
||||
this._sendResponse(cmd.id, 'OK', cmd.quiet ?? 0);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case KittyAction.TRANSMIT_DISPLAY:
|
||||
return this._handleTransmitDisplay(cmd, bytes, decodeError);
|
||||
case KittyAction.QUERY:
|
||||
return this._handleQuery(cmd, bytes, decodeError);
|
||||
case KittyAction.PLACEMENT:
|
||||
// a=p ignores any payload — image data was already transmitted
|
||||
return this._handlePlacement(cmd);
|
||||
default:
|
||||
// TODO: Implement remaining actions when needed:
|
||||
// - a=f (frame): animation frame operations
|
||||
// - a=a (animation): animation control
|
||||
// - a=c (compose): compose images
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePlacement(cmd: IKittyCommand): boolean | Promise<boolean> {
|
||||
if (cmd.id === undefined) {
|
||||
return true;
|
||||
}
|
||||
const image = this._kittyStorage.getImage(cmd.id);
|
||||
if (!image) {
|
||||
this._sendResponse(cmd.id, 'ENOENT:image not found', cmd.quiet ?? 0, cmd.placementId);
|
||||
return true;
|
||||
}
|
||||
const result = this._displayImage(image, cmd);
|
||||
return result.then(success => {
|
||||
this._sendResponse(cmd.id!, success ? 'OK' : 'EINVAL:image rendering failed', cmd.quiet ?? 0, cmd.placementId);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTransmit(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
|
||||
// TODO: Support file-based transmission modes (t=f, t=t, t=s)
|
||||
// Currently only supports direct transmission (t=d, the default).
|
||||
// - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
|
||||
// - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
|
||||
// - t=s: Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
|
||||
// These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
|
||||
// 1. Check cmd.transmission (t key) before treating bytes as image data
|
||||
// 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
|
||||
// 3. For t=d: treat bytes as image data (current behavior)
|
||||
// When implementing, also update _handleQuery to accept these transmission mediums.
|
||||
const transmission = cmd.transmission ?? 'd';
|
||||
if (transmission !== 'd') {
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:unsupported transmission medium', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (decodeError || bytes.length === 0) return true;
|
||||
|
||||
this._kittyStorage.storeImage(cmd.id, {
|
||||
data: new Blob([bytes as BlobPart]),
|
||||
width: cmd.width ?? 0,
|
||||
height: cmd.height ?? 0,
|
||||
format: (cmd.format ?? KittyFormat.RGBA) as 24 | 32 | 100,
|
||||
compression: cmd.compression ?? ''
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleTransmitDisplay(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
|
||||
if (decodeError) {
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
this._handleTransmit(cmd, bytes, decodeError);
|
||||
|
||||
const id = cmd.id ?? this._kittyStorage.lastImageId;
|
||||
const image = this._kittyStorage.getImage(id);
|
||||
if (image) {
|
||||
const result = this._displayImage(image, cmd);
|
||||
if (cmd.id !== undefined) {
|
||||
return result.then(success => {
|
||||
this._sendResponse(id, success ? 'OK' : 'EINVAL:image rendering failed', cmd.quiet ?? 0);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return result.then(() => true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleQuery(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
|
||||
const id = cmd.id ?? 0;
|
||||
const quiet = cmd.quiet ?? 0;
|
||||
|
||||
// Per spec: reject unsupported transmission mediums (only t=d is supported atm)
|
||||
// TODO: When filesystem support is added (Node.js/Electron), update this to accept
|
||||
// t=f (file), t=t (temp file), and t=s (shared memory) and respond OK for queries.
|
||||
const transmission = cmd.transmission ?? 'd';
|
||||
if (transmission !== 'd') {
|
||||
this._sendResponse(id, 'EINVAL:unsupported transmission medium', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check decode error first (invalid base64)
|
||||
if (decodeError) {
|
||||
this._sendResponse(id, 'EINVAL:invalid base64 data', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Capability query (no payload) - just respond OK
|
||||
if (bytes.length === 0) {
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
const format = cmd.format ?? KittyFormat.RGBA;
|
||||
|
||||
if (format === KittyFormat.PNG) {
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
} else {
|
||||
const width = cmd.width ?? 0;
|
||||
const height = cmd.height ?? 0;
|
||||
|
||||
if (!width || !height) {
|
||||
this._sendResponse(id, 'EINVAL:width and height required for raw pixel data', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
const bytesPerPixel = format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
|
||||
const expectedBytes = width * height * bytesPerPixel;
|
||||
|
||||
if (bytes.length < expectedBytes) {
|
||||
this._sendResponse(id, `EINVAL:insufficient pixel data`, quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleDelete(cmd: IKittyCommand): boolean {
|
||||
// Per spec: default delete selector is 'a' (delete all visible placements)
|
||||
const selector = cmd.deleteSelector ?? 'a';
|
||||
|
||||
// TODO: Distinguish lowercase (delete placements only) from uppercase
|
||||
// (delete placements + free stored image data). Currently both variants
|
||||
// free everything since we don't separate stored data from placements.
|
||||
switch (selector) {
|
||||
case 'a':
|
||||
case 'A':
|
||||
this._cleanupAllPending();
|
||||
this._kittyStorage.deleteAll();
|
||||
break;
|
||||
case 'i':
|
||||
case 'I':
|
||||
if (cmd.id !== undefined) {
|
||||
const pending = this._pendingTransmissions.get(cmd.id);
|
||||
if (pending) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._removePendingEntry(cmd.id);
|
||||
this._kittyStorage.deleteById(cmd.id);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unsupported selectors (c, n, p, q, r, x, y, z, f) — ignore for now
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _sendResponse(id: number, message: string, quiet: number, placementId?: number): void {
|
||||
const isOk = message === 'OK';
|
||||
if (isOk && quiet === 1) return;
|
||||
if (!isOk && quiet === 2) return;
|
||||
|
||||
const pPart = placementId ? `,p=${placementId}` : '';
|
||||
const response = `\x1b_Gi=${id}${pPart};${message}\x1b\\`;
|
||||
this._coreTerminal._core.coreService.triggerDataEvent(response);
|
||||
}
|
||||
|
||||
// Image display
|
||||
|
||||
private _displayImage(image: IKittyImageData, cmd: IKittyCommand): Promise<boolean> {
|
||||
return this._decodeAndDisplay(image, cmd)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
private async _decodeAndDisplay(image: IKittyImageData, cmd: IKittyCommand): Promise<void> {
|
||||
let bitmap = await this._createBitmap(image);
|
||||
|
||||
const cropX = Math.max(0, cmd.x ?? 0);
|
||||
const cropY = Math.max(0, cmd.y ?? 0);
|
||||
const cropW = cmd.sourceWidth || (bitmap.width - cropX);
|
||||
const cropH = cmd.sourceHeight || (bitmap.height - cropY);
|
||||
|
||||
const maxCropW = Math.max(0, bitmap.width - cropX);
|
||||
const maxCropH = Math.max(0, bitmap.height - cropY);
|
||||
const finalCropW = Math.max(0, Math.min(cropW, maxCropW));
|
||||
const finalCropH = Math.max(0, Math.min(cropH, maxCropH));
|
||||
|
||||
if (finalCropW === 0 || finalCropH === 0) {
|
||||
bitmap.close();
|
||||
throw new Error('invalid source rectangle');
|
||||
}
|
||||
|
||||
if (cropX !== 0 || cropY !== 0 || finalCropW !== bitmap.width || finalCropH !== bitmap.height) {
|
||||
const cropped = await createImageBitmap(bitmap, cropX, cropY, finalCropW, finalCropH);
|
||||
bitmap.close();
|
||||
bitmap = cropped;
|
||||
}
|
||||
|
||||
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
||||
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
||||
|
||||
// Per spec: c/r default to image's natural cell dimensions.
|
||||
// If only one of c/r is specified, compute the other from image aspect ratio.
|
||||
let imgCols: number;
|
||||
let imgRows: number;
|
||||
if (cmd.columns !== undefined && cmd.rows !== undefined) {
|
||||
imgCols = cmd.columns;
|
||||
imgRows = cmd.rows;
|
||||
} else if (cmd.columns !== undefined) {
|
||||
imgCols = cmd.columns;
|
||||
imgRows = Math.max(1, Math.ceil((bitmap.height / bitmap.width) * (imgCols * cw) / ch));
|
||||
} else if (cmd.rows !== undefined) {
|
||||
imgRows = cmd.rows;
|
||||
imgCols = Math.max(1, Math.ceil((bitmap.width / bitmap.height) * (imgRows * ch) / cw));
|
||||
} else {
|
||||
imgCols = Math.ceil(bitmap.width / cw);
|
||||
imgRows = Math.ceil(bitmap.height / ch);
|
||||
}
|
||||
|
||||
let w = bitmap.width;
|
||||
let h = bitmap.height;
|
||||
|
||||
// Scale bitmap to fit placement rectangle when c/r are specified
|
||||
if (cmd.columns !== undefined || cmd.rows !== undefined) {
|
||||
w = Math.round(imgCols * cw);
|
||||
h = Math.round(imgRows * ch);
|
||||
}
|
||||
|
||||
if (w * h > this._opts.pixelLimit) {
|
||||
bitmap.close();
|
||||
throw new Error('image exceeds pixel limit');
|
||||
}
|
||||
|
||||
// Save cursor position before addImage modifies it
|
||||
const buffer = this._coreTerminal._core.buffer;
|
||||
const savedX = buffer.x;
|
||||
const savedY = buffer.y;
|
||||
const savedYbase = buffer.ybase;
|
||||
|
||||
// Determine layer based on z-index: negative = behind text, 0+ = on top.
|
||||
// When z<0 we always use the bottom layer even without allowTransparency —
|
||||
// the image will simply be hidden behind the opaque text background, which
|
||||
// is the correct behavior (client asked for "behind text").
|
||||
const wantsBottom = cmd.zIndex !== undefined && cmd.zIndex < 0;
|
||||
const layer: ImageLayer = wantsBottom ? 'bottom' : 'top';
|
||||
|
||||
let finalBitmap = bitmap;
|
||||
if (w !== bitmap.width || h !== bitmap.height) {
|
||||
finalBitmap = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
|
||||
bitmap.close();
|
||||
}
|
||||
|
||||
// Per spec: X/Y are pixel offsets within the first cell, so clamp to cell dimensions
|
||||
const xOffset = Math.min(Math.max(0, cmd.xOffset ?? 0), cw - 1);
|
||||
const yOffset = Math.min(Math.max(0, cmd.yOffset ?? 0), ch - 1);
|
||||
if (xOffset !== 0 || yOffset !== 0) {
|
||||
// Per spec: X/Y is not added to c/r area. When c/r are explicit, the
|
||||
// total placement area remains c*cw × r*ch pixels and the offset image
|
||||
// is clipped to fit. When c/r are unset, the padded canvas determines
|
||||
// the natural cell dimensions.
|
||||
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : finalBitmap.width + xOffset;
|
||||
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : finalBitmap.height + yOffset;
|
||||
const offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
|
||||
const offsetCtx = offsetCanvas.getContext('2d');
|
||||
if (!offsetCtx) {
|
||||
finalBitmap.close();
|
||||
throw new Error('Failed to create offset canvas context');
|
||||
}
|
||||
offsetCtx.drawImage(finalBitmap, xOffset, yOffset);
|
||||
|
||||
const offsetBitmap = await createImageBitmap(offsetCanvas);
|
||||
offsetCanvas.width = offsetCanvas.height = 0;
|
||||
finalBitmap.close();
|
||||
finalBitmap = offsetBitmap;
|
||||
w = finalBitmap.width;
|
||||
h = finalBitmap.height;
|
||||
if (w * h > this._opts.pixelLimit) {
|
||||
finalBitmap.close();
|
||||
throw new Error('image exceeds pixel limit');
|
||||
}
|
||||
if (cmd.columns === undefined) {
|
||||
imgCols = Math.ceil(finalBitmap.width / cw);
|
||||
}
|
||||
if (cmd.rows === undefined) {
|
||||
imgRows = Math.ceil(finalBitmap.height / ch);
|
||||
}
|
||||
}
|
||||
|
||||
const zIndex = cmd.zIndex ?? 0;
|
||||
this._kittyStorage.addImage(image.id, finalBitmap, true, layer, zIndex);
|
||||
|
||||
// Kitty cursor movement
|
||||
// Per spec: cursor placed at first column after last image column,
|
||||
// on the last row of the image. C=1 means don't move cursor.
|
||||
if (cmd.cursorMovement === 1) {
|
||||
// C=1: restore cursor to position before image was placed
|
||||
const scrolled = buffer.ybase - savedYbase;
|
||||
buffer.x = savedX;
|
||||
// Can't restore cursor to scrollback?
|
||||
buffer.y = Math.max(savedY - scrolled, 0);
|
||||
} else {
|
||||
// Default (C=0): advance cursor horizontally past the image
|
||||
// addImage already positioned cursor on the last row via lineFeeds
|
||||
buffer.x = Math.min(savedX + imgCols, this._coreTerminal.cols);
|
||||
}
|
||||
}
|
||||
|
||||
// Create ImageBitmap from already-decoded image data.
|
||||
private async _createBitmap(image: IKittyImageData): Promise<ImageBitmap> {
|
||||
let bytes: Uint8Array = new Uint8Array(await image.data.arrayBuffer());
|
||||
|
||||
if (image.compression === KittyCompression.ZLIB) {
|
||||
bytes = await this._decompressZlib(bytes);
|
||||
}
|
||||
|
||||
if (image.format === KittyFormat.PNG) {
|
||||
const blob = new Blob([bytes as BlobPart], { type: 'image/png' });
|
||||
if (!window.createImageBitmap) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
return new Promise<ImageBitmap>((resolve, reject) => {
|
||||
img.addEventListener('load', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
const canvas = ImageRenderer.createCanvas(window.document, img.width, img.height);
|
||||
canvas.getContext('2d')?.drawImage(img, 0, 0);
|
||||
createImageBitmap(canvas).then(resolve).catch(reject);
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('Failed to load image'));
|
||||
});
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
return createImageBitmap(blob);
|
||||
}
|
||||
|
||||
// Raw pixel data
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
|
||||
if (!width || !height) {
|
||||
throw new Error('Width and height required for raw pixel data');
|
||||
}
|
||||
|
||||
const bytesPerPixel = image.format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
|
||||
const expectedBytes = width * height * bytesPerPixel;
|
||||
|
||||
if (bytes.length < expectedBytes) {
|
||||
throw new Error('Insufficient pixel data');
|
||||
}
|
||||
|
||||
const pixelCount = width * height;
|
||||
|
||||
if (image.format === KittyFormat.RGBA) {
|
||||
// RGBA: use bytes directly — no copy needed
|
||||
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * BYTES_PER_PIXEL_RGBA), width, height));
|
||||
}
|
||||
|
||||
// RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
|
||||
// 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
|
||||
// Assumes little-endian (all modern browsers/Node.js).
|
||||
const data = new Uint8ClampedArray(pixelCount * BYTES_PER_PIXEL_RGBA);
|
||||
const src32 = new Uint32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
|
||||
const dst32 = new Uint32Array(data.buffer);
|
||||
const alignedPixels = pixelCount & ~3; // round down to multiple of 4
|
||||
|
||||
let srcOffset = 0;
|
||||
let dstOffset = 0;
|
||||
for (let i = 0; i < alignedPixels; i += 4) {
|
||||
const b0 = src32[srcOffset++];
|
||||
const b1 = src32[srcOffset++];
|
||||
const b2 = src32[srcOffset++];
|
||||
// Little-endian: pixel bytes are [R,G,B] → uint32 ABGR layout
|
||||
dst32[dstOffset++] = (b0 & 0x00FFFFFF) | 0xFF000000;
|
||||
dst32[dstOffset++] = ((b0 >>> 24) | (b1 << 8)) & 0x00FFFFFF | 0xFF000000;
|
||||
dst32[dstOffset++] = ((b1 >>> 16) | (b2 << 16)) & 0x00FFFFFF | 0xFF000000;
|
||||
dst32[dstOffset++] = (b2 >>> 8) | 0xFF000000;
|
||||
}
|
||||
|
||||
// Handle remaining 1–3 pixels
|
||||
let srcByte = alignedPixels * BYTES_PER_PIXEL_RGB;
|
||||
let dstByte = alignedPixels * BYTES_PER_PIXEL_RGBA;
|
||||
for (let i = alignedPixels; i < pixelCount; i++) {
|
||||
data[dstByte] = bytes[srcByte];
|
||||
data[dstByte + 1] = bytes[srcByte + 1];
|
||||
data[dstByte + 2] = bytes[srcByte + 2];
|
||||
data[dstByte + 3] = ALPHA_OPAQUE;
|
||||
srcByte += BYTES_PER_PIXEL_RGB;
|
||||
dstByte += BYTES_PER_PIXEL_RGBA;
|
||||
}
|
||||
|
||||
return createImageBitmap(new ImageData(data, width, height));
|
||||
}
|
||||
|
||||
private async _decompressZlib(compressed: Uint8Array): Promise<Uint8Array> {
|
||||
try {
|
||||
return await this._decompress(compressed, 'deflate');
|
||||
} catch {
|
||||
return await this._decompress(compressed, 'deflate-raw');
|
||||
}
|
||||
}
|
||||
|
||||
private async _decompress(compressed: Uint8Array, format: 'deflate' | 'deflate-raw'): Promise<Uint8Array> {
|
||||
const ds = new DecompressionStream(format);
|
||||
const writer = ds.writable.getWriter();
|
||||
writer.write(compressed as BufferSource);
|
||||
writer.close();
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
const reader = ds.readable.getReader();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public get images(): ReadonlyMap<number, IKittyImageData> {
|
||||
return this._kittyStorage.images;
|
||||
}
|
||||
|
||||
public get _kittyIdToStorageId(): ReadonlyMap<number, number> {
|
||||
return this._kittyStorage.kittyIdToStorageId;
|
||||
}
|
||||
|
||||
public get pendingTransmissions(): ReadonlyMap<number, IPendingTransmission> {
|
||||
return this._pendingTransmissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { parseKittyCommand, KittyAction, KittyFormat } from './KittyGraphicsTypes';
|
||||
|
||||
describe('KittyGraphicsTypes', () => {
|
||||
describe('parseKittyCommand', () => {
|
||||
it('should parse control data with action and format', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100');
|
||||
assert.strictEqual(cmd.action, 'T');
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
});
|
||||
|
||||
it('should parse control data with all options', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=32,i=5,s=10,v=20,c=3,r=2,m=1,q=2');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.strictEqual(cmd.format, 32);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
assert.strictEqual(cmd.width, 10);
|
||||
assert.strictEqual(cmd.height, 20);
|
||||
assert.strictEqual(cmd.columns, 3);
|
||||
assert.strictEqual(cmd.rows, 2);
|
||||
assert.strictEqual(cmd.more, 1);
|
||||
assert.strictEqual(cmd.quiet, 2);
|
||||
});
|
||||
|
||||
it('should handle empty control data', () => {
|
||||
const cmd = parseKittyCommand('');
|
||||
assert.strictEqual(cmd.action, undefined);
|
||||
assert.strictEqual(cmd.format, undefined);
|
||||
});
|
||||
|
||||
it('should parse transmit action', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=100');
|
||||
assert.strictEqual(cmd.action, KittyAction.TRANSMIT);
|
||||
assert.strictEqual(cmd.format, KittyFormat.PNG);
|
||||
});
|
||||
|
||||
it('should parse delete action', () => {
|
||||
const cmd = parseKittyCommand('a=d,i=5');
|
||||
assert.strictEqual(cmd.action, KittyAction.DELETE);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse empty action as empty string', () => {
|
||||
const cmd = parseKittyCommand('a=,f=100');
|
||||
assert.strictEqual(cmd.action, '');
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
});
|
||||
|
||||
it('should leave action undefined when key is not present', () => {
|
||||
const cmd = parseKittyCommand('f=100,i=5');
|
||||
assert.strictEqual(cmd.action, undefined);
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse compression key', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=32,o=z');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.strictEqual(cmd.format, 32);
|
||||
assert.strictEqual(cmd.compression, 'z');
|
||||
});
|
||||
|
||||
it('should parse cursor movement key', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,C=1');
|
||||
assert.strictEqual(cmd.cursorMovement, 1);
|
||||
});
|
||||
|
||||
it('should parse cursor movement key C=0', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,C=0');
|
||||
assert.strictEqual(cmd.cursorMovement, 0);
|
||||
});
|
||||
|
||||
it('should parse x and y offset', () => {
|
||||
const cmd = parseKittyCommand('a=T,x=10,y=20');
|
||||
assert.strictEqual(cmd.x, 10);
|
||||
assert.strictEqual(cmd.y, 20);
|
||||
});
|
||||
|
||||
it('should handle keys without values', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=,i=5');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.ok(isNaN(cmd.format!));
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse z-index key with positive value', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,z=10');
|
||||
assert.strictEqual(cmd.zIndex, 10);
|
||||
});
|
||||
|
||||
it('should parse z-index key with zero', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,z=0');
|
||||
assert.strictEqual(cmd.zIndex, 0);
|
||||
});
|
||||
|
||||
it('should parse z-index key with negative value', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,z=-1');
|
||||
assert.strictEqual(cmd.zIndex, -1);
|
||||
});
|
||||
|
||||
it('should leave zIndex undefined when not specified', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100');
|
||||
assert.strictEqual(cmd.zIndex, undefined);
|
||||
});
|
||||
|
||||
it('should parse delete selector key', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5');
|
||||
assert.strictEqual(cmd.action, 'd');
|
||||
assert.strictEqual(cmd.deleteSelector, 'i');
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse uppercase delete selector', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=A');
|
||||
assert.strictEqual(cmd.deleteSelector, 'A');
|
||||
});
|
||||
|
||||
it('should parse delete selector d=a (all)', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=a');
|
||||
assert.strictEqual(cmd.deleteSelector, 'a');
|
||||
});
|
||||
|
||||
it('should leave deleteSelector undefined when not specified', () => {
|
||||
const cmd = parseKittyCommand('a=d,i=5');
|
||||
assert.strictEqual(cmd.deleteSelector, undefined);
|
||||
});
|
||||
|
||||
it('should parse placement id key', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5,p=3');
|
||||
assert.strictEqual(cmd.placementId, 3);
|
||||
assert.strictEqual(cmd.deleteSelector, 'i');
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should leave placementId undefined when not specified', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5');
|
||||
assert.strictEqual(cmd.placementId, undefined);
|
||||
});
|
||||
|
||||
it('should parse image number key', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=100,I=42');
|
||||
assert.strictEqual(cmd.imageNumber, 42);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Kitty graphics protocol types, constants, and parsing utilities.
|
||||
*/
|
||||
|
||||
import type Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
|
||||
// Kitty graphics protocol action types.
|
||||
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
|
||||
export const enum KittyAction {
|
||||
TRANSMIT = 't',
|
||||
TRANSMIT_DISPLAY = 'T',
|
||||
QUERY = 'q',
|
||||
PLACEMENT = 'p',
|
||||
DELETE = 'd'
|
||||
}
|
||||
|
||||
// Kitty graphics protocol format types.
|
||||
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
|
||||
export const enum KittyFormat {
|
||||
RGB = 24,
|
||||
RGBA = 32,
|
||||
PNG = 100
|
||||
}
|
||||
|
||||
// Kitty graphics protocol compression types.
|
||||
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
|
||||
export const enum KittyCompression {
|
||||
NONE = '',
|
||||
ZLIB = 'z'
|
||||
}
|
||||
|
||||
// Kitty graphics protocol control data keys.
|
||||
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
|
||||
export const enum KittyKey {
|
||||
// Action to perform (t=transmit, T=transmit+display, q=query, p=placement, d=delete)
|
||||
ACTION = 'a',
|
||||
// Image format (24=RGB, 32=RGBA, 100=PNG)
|
||||
FORMAT = 'f',
|
||||
// Image ID for referencing stored images
|
||||
ID = 'i',
|
||||
// Image number (alternative to ID, terminal assigns ID)
|
||||
IMAGE_NUMBER = 'I',
|
||||
// Source image width in pixels
|
||||
WIDTH = 's',
|
||||
// Source image height in pixels
|
||||
HEIGHT = 'v',
|
||||
// The left edge (in pixels) of the image area to display
|
||||
X_OFFSET = 'x',
|
||||
// The top edge (in pixels) of the image area to display
|
||||
Y_OFFSET = 'y',
|
||||
// Width (in pixels) of the source rectangle to display
|
||||
SOURCE_WIDTH = 'w',
|
||||
// Height (in pixels) of the source rectangle to display
|
||||
SOURCE_HEIGHT = 'h',
|
||||
// Horizontal offset (in pixels) within the first cell
|
||||
X_PLACEMENT_OFFSET = 'X',
|
||||
// Vertical offset (in pixels) within the first cell
|
||||
Y_PLACEMENT_OFFSET = 'Y',
|
||||
// Number of terminal columns to display the image over
|
||||
COLUMNS = 'c',
|
||||
// Number of terminal rows to display the image over
|
||||
ROWS = 'r',
|
||||
// More data flag (1=more chunks coming, 0=final chunk)
|
||||
MORE = 'm',
|
||||
// Compression type (z=zlib). This is essential for chunking larger images.
|
||||
COMPRESSION = 'o',
|
||||
// Quiet mode (1=suppress OK responses, 2=suppress error responses)
|
||||
QUIET = 'q',
|
||||
// Cursor movement policy (0=move cursor after image, 1=don't move cursor)
|
||||
CURSOR_MOVEMENT = 'C',
|
||||
// Z-index for image layering (negative = behind text, 0+ = on top)
|
||||
Z_INDEX = 'z',
|
||||
// Transmission medium (d=direct, f=file, t=temp file, s=shared memory)
|
||||
TRANSMISSION = 't',
|
||||
// Delete selector (a/A=all, i/I=by id, c/C=at cursor, etc.) — only used when a=d
|
||||
DELETE_SELECTOR = 'd',
|
||||
// Placement ID for targeting specific placements
|
||||
PLACEMENT_ID = 'p'
|
||||
}
|
||||
|
||||
// Pixel format constants
|
||||
export const BYTES_PER_PIXEL_RGB = 3;
|
||||
export const BYTES_PER_PIXEL_RGBA = 4;
|
||||
export const ALPHA_OPAQUE = 255;
|
||||
|
||||
// Parsed Kitty graphics command.
|
||||
export interface IKittyCommand {
|
||||
action?: string;
|
||||
format?: number;
|
||||
id?: number;
|
||||
imageNumber?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
sourceWidth?: number;
|
||||
sourceHeight?: number;
|
||||
xOffset?: number;
|
||||
yOffset?: number;
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
more?: number;
|
||||
quiet?: number;
|
||||
cursorMovement?: number;
|
||||
zIndex?: number;
|
||||
transmission?: string;
|
||||
deleteSelector?: string;
|
||||
placementId?: number;
|
||||
compression?: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
// Pending chunked transmission state.
|
||||
// Stores metadata from the first chunk while accumulating decoded payload data.
|
||||
export interface IPendingTransmission {
|
||||
// The parsed command from the first chunk (contains action, format, dimensions, etc.)
|
||||
cmd: IKittyCommand;
|
||||
// Decoder used across chunked payloads
|
||||
decoder: Base64Decoder;
|
||||
// Total encoded (base64) bytes received across all chunks - for size limit enforcement
|
||||
totalEncodedSize: number;
|
||||
// Whether any chunk has failed to decode
|
||||
decodeError: boolean;
|
||||
}
|
||||
|
||||
// Stored Kitty image data.
|
||||
export interface IKittyImageData {
|
||||
id: number;
|
||||
// Decoded image data stored as Blob (off JS heap) to avoid 2GB heap limit
|
||||
data: Blob;
|
||||
width: number;
|
||||
height: number;
|
||||
format: 24 | 32 | 100;
|
||||
compression?: string;
|
||||
}
|
||||
|
||||
// Parses Kitty graphics control data into a command object.
|
||||
export function parseKittyCommand(data: string): IKittyCommand {
|
||||
const cmd: IKittyCommand = {};
|
||||
const parts = data.split(',');
|
||||
|
||||
for (const part of parts) {
|
||||
const eqIdx = part.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
|
||||
const key = part.substring(0, eqIdx);
|
||||
const value = part.substring(eqIdx + 1);
|
||||
|
||||
// Handle string keys first
|
||||
if (key === KittyKey.ACTION) {
|
||||
cmd.action = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.COMPRESSION) {
|
||||
cmd.compression = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.TRANSMISSION) {
|
||||
cmd.transmission = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.DELETE_SELECTOR) {
|
||||
cmd.deleteSelector = value;
|
||||
continue;
|
||||
}
|
||||
const numValue = parseInt(value);
|
||||
switch (key) {
|
||||
case KittyKey.FORMAT: cmd.format = numValue; break;
|
||||
case KittyKey.ID: cmd.id = numValue; break;
|
||||
case KittyKey.IMAGE_NUMBER: cmd.imageNumber = numValue; break;
|
||||
case KittyKey.WIDTH: cmd.width = numValue; break;
|
||||
case KittyKey.HEIGHT: cmd.height = numValue; break;
|
||||
case KittyKey.X_OFFSET: cmd.x = numValue; break;
|
||||
case KittyKey.Y_OFFSET: cmd.y = numValue; break;
|
||||
case KittyKey.SOURCE_WIDTH: cmd.sourceWidth = numValue; break;
|
||||
case KittyKey.SOURCE_HEIGHT: cmd.sourceHeight = numValue; break;
|
||||
case KittyKey.X_PLACEMENT_OFFSET: cmd.xOffset = numValue; break;
|
||||
case KittyKey.Y_PLACEMENT_OFFSET: cmd.yOffset = numValue; break;
|
||||
case KittyKey.COLUMNS: cmd.columns = numValue; break;
|
||||
case KittyKey.ROWS: cmd.rows = numValue; break;
|
||||
case KittyKey.MORE: cmd.more = numValue; break;
|
||||
case KittyKey.QUIET: cmd.quiet = numValue; break;
|
||||
case KittyKey.CURSOR_MOVEMENT: cmd.cursorMovement = numValue; break;
|
||||
case KittyKey.Z_INDEX: cmd.zIndex = numValue; break;
|
||||
case KittyKey.PLACEMENT_ID: cmd.placementId = numValue; break;
|
||||
}
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ImageStorage } from '../ImageStorage';
|
||||
import { ImageLayer } from '../Types';
|
||||
import { IKittyImageData } from './KittyGraphicsTypes';
|
||||
|
||||
// Kitty-specific image storage controller.
|
||||
//
|
||||
// Wraps shared ImageStorage with kitty protocol semantics:
|
||||
// - tracks transmitted image payloads by kitty image id
|
||||
// - tracks kitty image id -> shared ImageStorage id mapping for displayed images
|
||||
// - mirrors shared-storage evictions into kitty maps
|
||||
// - applies protocol-level undisplayed-image eviction policy
|
||||
export class KittyImageStorage implements IDisposable {
|
||||
private static readonly _maxStoredImages = 256;
|
||||
|
||||
private _nextImageId = 1;
|
||||
private readonly _images: Map<number, IKittyImageData> = new Map();
|
||||
private readonly _kittyIdToStorageId: Map<number, number> = new Map();
|
||||
private readonly _storageIdToKittyId: Map<number, number> = new Map();
|
||||
|
||||
private readonly _previousOnImageDeleted: ((storageId: number) => void) | undefined;
|
||||
private readonly _wrappedOnImageDeleted: (storageId: number) => void;
|
||||
private readonly _handleStorageImageDeleted = (storageId: number): void => {
|
||||
const kittyId = this._storageIdToKittyId.get(storageId);
|
||||
if (kittyId !== undefined) {
|
||||
this._kittyIdToStorageId.delete(kittyId);
|
||||
this._storageIdToKittyId.delete(storageId);
|
||||
this._images.delete(kittyId);
|
||||
}
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage
|
||||
) {
|
||||
this._previousOnImageDeleted = this._storage.onImageDeleted;
|
||||
this._wrappedOnImageDeleted = (storageId: number) => {
|
||||
this._previousOnImageDeleted?.(storageId);
|
||||
this._handleStorageImageDeleted(storageId);
|
||||
};
|
||||
this._storage.onImageDeleted = this._wrappedOnImageDeleted;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._nextImageId = 1;
|
||||
this._images.clear();
|
||||
this._kittyIdToStorageId.clear();
|
||||
this._storageIdToKittyId.clear();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
if (this._storage.onImageDeleted === this._wrappedOnImageDeleted) {
|
||||
this._storage.onImageDeleted = this._previousOnImageDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
public storeImage(id: number | undefined, imageData: Omit<IKittyImageData, 'id'>): number {
|
||||
const imageId = id ?? this._nextImageId++;
|
||||
|
||||
const oldStorageId = this._kittyIdToStorageId.get(imageId);
|
||||
if (oldStorageId !== undefined) {
|
||||
this._storage.deleteImage(oldStorageId);
|
||||
this._kittyIdToStorageId.delete(imageId);
|
||||
this._storageIdToKittyId.delete(oldStorageId);
|
||||
}
|
||||
|
||||
if (!this._images.has(imageId) && this._images.size >= KittyImageStorage._maxStoredImages) {
|
||||
this._evictUndisplayedImages();
|
||||
}
|
||||
|
||||
this._images.set(imageId, {
|
||||
...imageData,
|
||||
id: imageId
|
||||
});
|
||||
return imageId;
|
||||
}
|
||||
|
||||
public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void {
|
||||
// Clean up stale reverse-mapping from a previous placement of the same
|
||||
// kitty image. The old shared-storage entry is kept (it may still be
|
||||
// visible on screen) but its reverse mapping is removed so that eviction
|
||||
// of the old entry won't incorrectly delete the kitty image data.
|
||||
const oldStorageId = this._kittyIdToStorageId.get(kittyId);
|
||||
if (oldStorageId !== undefined) {
|
||||
this._storageIdToKittyId.delete(oldStorageId);
|
||||
}
|
||||
const storageId = this._storage.addImage(image, scrolling, layer, zIndex);
|
||||
this._kittyIdToStorageId.set(kittyId, storageId);
|
||||
this._storageIdToKittyId.set(storageId, kittyId);
|
||||
}
|
||||
|
||||
public getImage(kittyId: number): IKittyImageData | undefined {
|
||||
return this._images.get(kittyId);
|
||||
}
|
||||
|
||||
public deleteById(kittyId: number): void {
|
||||
this._images.delete(kittyId);
|
||||
const storageId = this._kittyIdToStorageId.get(kittyId);
|
||||
if (storageId !== undefined) {
|
||||
this._storage.deleteImage(storageId);
|
||||
this._kittyIdToStorageId.delete(kittyId);
|
||||
this._storageIdToKittyId.delete(storageId);
|
||||
}
|
||||
}
|
||||
|
||||
public deleteAll(): void {
|
||||
this._images.clear();
|
||||
for (const storageId of this._kittyIdToStorageId.values()) {
|
||||
this._storage.deleteImage(storageId);
|
||||
}
|
||||
this._kittyIdToStorageId.clear();
|
||||
this._storageIdToKittyId.clear();
|
||||
}
|
||||
|
||||
public get images(): ReadonlyMap<number, IKittyImageData> {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
public get kittyIdToStorageId(): ReadonlyMap<number, number> {
|
||||
return this._kittyIdToStorageId;
|
||||
}
|
||||
|
||||
public get lastImageId(): number {
|
||||
return this._nextImageId - 1;
|
||||
}
|
||||
|
||||
private _evictUndisplayedImages(): void {
|
||||
for (const [kittyId] of this._images) {
|
||||
if (this._images.size <= KittyImageStorage._maxStoredImages / 2) {
|
||||
break;
|
||||
}
|
||||
if (!this._kittyIdToStorageId.has(kittyId)) {
|
||||
this._images.delete(kittyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,11 @@
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"browser/*": [ "../../../src/browser/*" ],
|
||||
"common/*": [ "../../../src/common/*" ],
|
||||
"vs/*": [ "../../../src/vs/*" ],
|
||||
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ]
|
||||
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ],
|
||||
"*": [ "./*" ]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
@@ -25,7 +24,6 @@
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/browser" },
|
||||
{ "path": "../../../src/common" },
|
||||
{ "path": "../../../src/vs" }
|
||||
{ "path": "../../../src/common" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface IImageAddonOptions {
|
||||
sixelSizeLimit: number;
|
||||
iipSupport: boolean;
|
||||
iipSizeLimit: number;
|
||||
kittySupport: boolean;
|
||||
kittySizeLimit: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -134,7 +136,9 @@ test.describe('ImageAddon', () => {
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000
|
||||
iipSizeLimit: 20000000,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 20000000
|
||||
};
|
||||
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
|
||||
});
|
||||
@@ -149,7 +153,9 @@ test.describe('ImageAddon', () => {
|
||||
storageLimit: 10,
|
||||
showPlaceholder: false,
|
||||
iipSupport: false,
|
||||
iipSizeLimit: 1000
|
||||
iipSizeLimit: 1000,
|
||||
kittySupport: false,
|
||||
kittySizeLimit: 1000
|
||||
};
|
||||
await ctx.page.evaluate(opts => {
|
||||
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
+9
@@ -76,6 +76,15 @@ declare module '@xterm/addon-image' {
|
||||
iipSupport?: boolean;
|
||||
/** IIP sequence size limit (default 20000000 bytes). */
|
||||
iipSizeLimit?: number;
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol settings
|
||||
*/
|
||||
|
||||
/** Whether Kitty graphics protocol is enabled (default is true). */
|
||||
kittySupport?: boolean;
|
||||
/** Kitty image size limit in bytes (default 20000000 bytes). */
|
||||
kittySizeLimit?: number;
|
||||
}
|
||||
|
||||
export class ImageAddon implements ITerminalAddon {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"node": ">8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p src",
|
||||
"watch": "tsc -w -p src",
|
||||
"build": "tsgo -p src",
|
||||
"watch": "tsgo -w -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "webpack",
|
||||
"pretest": "npm run build",
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface ITerminalAddon {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
|
||||
export class LigaturesAddon implements ITerminalAddon, ILigaturesApi {
|
||||
private readonly _fallbackLigatures: string[];
|
||||
private readonly _fontFeatureSettings?: string;
|
||||
|
||||
@@ -22,7 +22,7 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
|
||||
|
||||
constructor(options?: Partial<ILigatureOptions>) {
|
||||
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
|
||||
this._fallbackLigatures = (options?.fallbackLigatures || [
|
||||
this._fallbackLigatures = (options?.fallbackLigatures ?? [
|
||||
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
|
||||
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
|
||||
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
|
||||
|
||||
@@ -42,11 +42,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
if (status && status.state !== 'granted') {
|
||||
throw new Error('Permission to access local fonts not granted.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// A `TypeError` indicates the 'local-fonts'
|
||||
// permission is not yet implemented, so
|
||||
// only `throw` if this is _not_ the problem.
|
||||
if (err.name !== 'TypeError') {
|
||||
if (err instanceof Error && err.name !== 'TypeError') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,10 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
fonts[metadata.family].push(metadata);
|
||||
}
|
||||
fontsPromise = Promise.resolve(fonts);
|
||||
} catch (err: any) {
|
||||
console.error(err.name, err.message);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
|
||||
@@ -76,13 +78,13 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
fonts[metadata.family].push(metadata);
|
||||
}
|
||||
fontsPromise = Promise.resolve(fonts);
|
||||
} catch (err: any) {
|
||||
console.error(err.name, err.message);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fontsPromise) {
|
||||
fontsPromise = Promise.resolve({});
|
||||
}
|
||||
fontsPromise ??= Promise.resolve({});
|
||||
}
|
||||
|
||||
const fonts = await fontsPromise;
|
||||
|
||||
@@ -52,12 +52,10 @@ export function processLookaheadPosition(
|
||||
}
|
||||
processedEntries.add(currentEntry.entry);
|
||||
|
||||
if (!currentEntry.entry.forward) {
|
||||
currentEntry.entry.forward = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
}
|
||||
currentEntry.entry.forward ??= {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
// All glyphs at this position share ONE entry - lookahead just needs to match,
|
||||
// all paths lead to the same result
|
||||
@@ -97,12 +95,10 @@ export function processBacktrackPosition(
|
||||
}
|
||||
processedEntries.add(currentEntry.entry);
|
||||
|
||||
if (!currentEntry.entry.reverse) {
|
||||
currentEntry.entry.reverse = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
}
|
||||
currentEntry.entry.reverse ??= {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
// All glyphs at this position share ONE entry - backtrack just needs to match,
|
||||
// all paths lead to the same result
|
||||
|
||||
@@ -54,9 +54,6 @@ export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId
|
||||
return (glyphId + table.deltaGlyphId) % (2 ** 16);
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
|
||||
case 2:
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return table.substitute[coverageIndex] != null
|
||||
? table.substitute[coverageIndex]
|
||||
: null;
|
||||
return table.substitute[coverageIndex] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []
|
||||
let currentFontName: string | undefined = undefined;
|
||||
let font: Font | undefined = undefined;
|
||||
let loadingState: LoadingState = LoadingState.UNLOADED;
|
||||
let loadError: any | undefined = undefined;
|
||||
let loadError: unknown = undefined;
|
||||
|
||||
return term.registerCharacterJoiner((text: string): [number, number][] => {
|
||||
// If the font hasn't been loaded yet, load it and return an empty result
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
"outDir": "../out-esbuild-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
|
||||
import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress';
|
||||
import type { Emitter, Event } from 'vs/base/common/event';
|
||||
import type { Emitter, IEvent } from 'common/Event';
|
||||
|
||||
|
||||
const enum ProgressType {
|
||||
@@ -39,7 +39,7 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi {
|
||||
private _pr = 0;
|
||||
// HACK: This uses ! to align with the API, this should be fixed when 5283 is resolved
|
||||
private _onChange!: Emitter<IProgressState>;
|
||||
public onChange!: Event<IProgressState>;
|
||||
public onChange!: IEvent<IProgressState>;
|
||||
|
||||
public dispose(): void {
|
||||
this._seqHandler?.dispose();
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"vs/*": [
|
||||
"../../../src/vs/*"
|
||||
],
|
||||
"@xterm/addon-progress": [
|
||||
"../typings/addon-progress.d.ts"
|
||||
]
|
||||
@@ -35,7 +35,7 @@
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/vs"
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"prepackage": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "../../node_modules/.bin/tsgo -p .",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';
|
||||
import type { ISearchDecorationOptions } from '@xterm/addon-search';
|
||||
import { dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { dispose, Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import type { ISearchResult } from './SearchEngine';
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
|
||||
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { disposableTimeout } from 'vs/base/common/async';
|
||||
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';
|
||||
import { Emitter, type IEvent } from 'common/Event';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { disposableTimeout } from 'common/Async';
|
||||
import { SearchLineCache } from './SearchLineCache';
|
||||
import { SearchState } from './SearchState';
|
||||
import { SearchEngine, type ISearchResult } from './SearchEngine';
|
||||
@@ -47,7 +47,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
private readonly _onBeforeSearch = this._register(new Emitter<void>());
|
||||
public readonly onBeforeSearch = this._onBeforeSearch.event;
|
||||
|
||||
public get onDidChangeResults(): Event<ISearchResultChangeEvent> {
|
||||
public get onDidChangeResults(): IEvent<ISearchResultChangeEvent> {
|
||||
return this._resultTracker.onDidChangeResults;
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
* @param result The result to select.
|
||||
* @returns Whether a result was selected.
|
||||
*/
|
||||
private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean {
|
||||
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
|
||||
if (!this._terminal || !this._decorationManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SearchEngine } from './SearchEngine';
|
||||
import { SearchLineCache } from './SearchLineCache';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import type { ISearchOptions } from '@xterm/addon-search';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { DisposableStore } from 'common/Lifecycle';
|
||||
|
||||
function writeP(terminal: Terminal, data: string): Promise<void> {
|
||||
return new Promise(r => terminal.write(data, r));
|
||||
|
||||
@@ -198,9 +198,7 @@ export class SearchEngine {
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||
}
|
||||
result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||
|
||||
// Search from startRow - 1 to top
|
||||
if (!result) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { assert } from 'chai';
|
||||
import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { timeout } from 'common/Async';
|
||||
|
||||
function writeP(terminal: Terminal, data: string): Promise<void> {
|
||||
return new Promise(r => terminal.write(data, r));
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { disposableTimeout } from 'vs/base/common/async';
|
||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { disposableTimeout } from 'common/Async';
|
||||
|
||||
export type LineCacheEntry = [
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import type { ISearchResultChangeEvent } from '@xterm/addon-search';
|
||||
import type { IDisposable } from '@xterm/xterm';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, type IEvent } from 'common/Event';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import type { ISearchResult } from './SearchEngine';
|
||||
|
||||
/**
|
||||
@@ -25,7 +25,7 @@ export class SearchResultTracker extends Disposable {
|
||||
private _selectedDecoration: ISelectedDecoration | undefined;
|
||||
|
||||
private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
|
||||
public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
|
||||
public get onDidChangeResults(): IEvent<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
|
||||
|
||||
/**
|
||||
* Gets the current search results.
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"vs/*": [
|
||||
"../../../src/vs/*"
|
||||
],
|
||||
"@xterm/addon-search" : [
|
||||
"../typings/addon-search.d.ts"
|
||||
]
|
||||
@@ -39,9 +36,6 @@
|
||||
},
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/vs"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"dom",
|
||||
"es2021"
|
||||
],
|
||||
"rootDir": "..",
|
||||
"outDir": "../out-benchmark",
|
||||
"types": ["../../../node_modules/@types/node"],
|
||||
"moduleResolution": "node",
|
||||
"strict": false,
|
||||
"target": "es2021",
|
||||
"module": "commonjs",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('SerializeAddon', () => {
|
||||
describe('underline styles', () => {
|
||||
it('should serialize single underline with style', async () => {
|
||||
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:1mtest\u001b[0m');
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize double underline', async () => {
|
||||
@@ -228,7 +228,7 @@ describe('SerializeAddon', () => {
|
||||
it('empty terminal with selection turned off', () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.notEqual(output, '');
|
||||
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
|
||||
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) ?? []).length, 2);
|
||||
});
|
||||
|
||||
it('empty terminal with no selection', () => {
|
||||
@@ -245,7 +245,7 @@ describe('SerializeAddon', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('basic terminal with html unsafe chars', async () => {
|
||||
@@ -255,7 +255,7 @@ describe('SerializeAddon', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal((output.match(/<div><span><a>&pi;<\/span><\/div>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<div><span><a>&pi;<\/span><\/div>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('serializes rows within a provided range', async () => {
|
||||
@@ -267,7 +267,7 @@ describe('SerializeAddon', () => {
|
||||
startCol: 4
|
||||
}
|
||||
});
|
||||
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) || [];
|
||||
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) ?? [];
|
||||
assert.equal(rowMatches.length, 1, output);
|
||||
assert.ok(rowMatches[0]?.includes('hello'));
|
||||
assert.ok(!output.includes('bye'));
|
||||
@@ -278,56 +278,56 @@ describe('SerializeAddon', () => {
|
||||
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with italic styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with inverse styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with double underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:2') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline double;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: underline double;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with curly underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:3') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline wavy;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: underline wavy;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dotted underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:4') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline dotted;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: underline dotted;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dashed underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:5') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline dashed;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: underline dashed;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with underline color (palette)', async () => {
|
||||
@@ -350,49 +350,49 @@ describe('SerializeAddon', () => {
|
||||
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dim styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with strikethrough styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with combined styling', async () => {
|
||||
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) ?? []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) ?? []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with color styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with background styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with default options', async () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with custom options', async () => {
|
||||
@@ -405,14 +405,14 @@ describe('SerializeAddon', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with background included', async () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with custom color styling', async () => {
|
||||
@@ -422,7 +422,7 @@ describe('SerializeAddon', () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;0') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with color styling - xterm headless', async () => {
|
||||
@@ -432,7 +432,7 @@ describe('SerializeAddon', () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,11 +89,19 @@ function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell)
|
||||
if (!cell1.isUnderline() && !cell2.isUnderline()) {
|
||||
return true;
|
||||
}
|
||||
const cell1Data = cell1 as unknown as IAttributeData;
|
||||
const cell2Data = cell2 as unknown as IAttributeData;
|
||||
return cell1Data.getUnderlineStyle() === cell2Data.getUnderlineStyle()
|
||||
&& cell1Data.getUnderlineColor() === cell2Data.getUnderlineColor()
|
||||
&& cell1Data.getUnderlineColorMode() === cell2Data.getUnderlineColorMode();
|
||||
if (cell1.getUnderlineStyle() !== cell2.getUnderlineStyle()) {
|
||||
return false;
|
||||
}
|
||||
const cell1Default = cell1.isUnderlineColorDefault();
|
||||
const cell2Default = cell2.isUnderlineColorDefault();
|
||||
if (cell1Default && cell2Default) {
|
||||
return true;
|
||||
}
|
||||
if (cell1Default !== cell2Default) {
|
||||
return false;
|
||||
}
|
||||
return cell1.getUnderlineColor() === cell2.getUnderlineColor()
|
||||
&& cell1.getUnderlineColorMode() === cell2.getUnderlineColorMode();
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
@@ -109,6 +117,16 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
|
||||
&& cell1.isStrikethrough() === cell2.isStrikethrough();
|
||||
}
|
||||
|
||||
function attributesEquals(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
const cell1AsBufferCell = cell1 as IBufferCell;
|
||||
if (typeof cell1AsBufferCell.attributesEquals === 'function') {
|
||||
return cell1AsBufferCell.attributesEquals(cell2);
|
||||
}
|
||||
return equalFg(cell1, cell2)
|
||||
&& equalBg(cell1, cell2)
|
||||
&& equalFlags(cell1, cell2);
|
||||
}
|
||||
|
||||
class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _rowIndex: number = 0;
|
||||
private _allRows: string[] = new Array<string>();
|
||||
@@ -258,6 +276,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
|
||||
const sgrSeq: number[] = [];
|
||||
if (attributesEquals(cell, oldCell)) {
|
||||
return sgrSeq;
|
||||
}
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
const flagsChanged = !equalFlags(cell, oldCell);
|
||||
@@ -290,17 +311,18 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||
if (!equalUnderline(cell, oldCell)) {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
const style = cellData.getUnderlineStyle();
|
||||
const style = cell.getUnderlineStyle();
|
||||
if (style === UnderlineStyle.NONE) {
|
||||
sgrSeq.push(24);
|
||||
} else if (style === UnderlineStyle.SINGLE && cell.isUnderlineColorDefault()) {
|
||||
sgrSeq.push(4);
|
||||
} else {
|
||||
// Use SGR 4:X format for underline styles
|
||||
sgrSeq.push('4:' + style as unknown as number);
|
||||
// Handle underline color
|
||||
if (!cellData.isUnderlineColorDefault()) {
|
||||
const color = cellData.getUnderlineColor();
|
||||
if (cellData.isUnderlineColorRGB()) {
|
||||
if (!cell.isUnderlineColorDefault()) {
|
||||
const color = cell.getUnderlineColor();
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
|
||||
} else {
|
||||
sgrSeq.push('58:5:' + color as unknown as number);
|
||||
@@ -457,7 +479,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
export class SerializeAddon implements ITerminalAddon, ISerializeApi {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -601,7 +623,7 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
return this._serializeBufferAsHTML(this._terminal, options || {});
|
||||
return this._serializeBufferAsHTML(this._terminal, options ?? {});
|
||||
}
|
||||
|
||||
public dispose(): void { }
|
||||
@@ -630,20 +652,6 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private _padStart(target: string, targetLength: number, padString: string): string {
|
||||
targetLength = targetLength >> 0;
|
||||
padString = padString ?? ' ';
|
||||
if (target.length > targetLength) {
|
||||
return target;
|
||||
}
|
||||
|
||||
targetLength -= target.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length);
|
||||
}
|
||||
return padString.slice(0, targetLength) + target;
|
||||
}
|
||||
|
||||
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||
this._htmlContent += '<html><body><!--StartFragment--><pre>';
|
||||
|
||||
@@ -680,7 +688,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
(color >> 8) & 255,
|
||||
(color ) & 255
|
||||
];
|
||||
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
|
||||
return this._ansiColors[color].css;
|
||||
@@ -689,26 +697,24 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
|
||||
private _getUnderlineColor(cell: IBufferCell): string | undefined {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
if (cellData.isUnderlineColorDefault()) {
|
||||
if (cell.isUnderlineColorDefault()) {
|
||||
return undefined;
|
||||
}
|
||||
const color = cellData.getUnderlineColor();
|
||||
if (cellData.isUnderlineColorRGB()) {
|
||||
const color = cell.getUnderlineColor();
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
const rgb = [
|
||||
(color >> 16) & 255,
|
||||
(color >> 8) & 255,
|
||||
(color ) & 255
|
||||
];
|
||||
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
// Palette color
|
||||
return this._ansiColors[color].css;
|
||||
}
|
||||
|
||||
private _getUnderlineStyle(cell: IBufferCell): string {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
switch (cellData.getUnderlineStyle()) {
|
||||
switch (cell.getUnderlineStyle()) {
|
||||
case UnderlineStyle.SINGLE:
|
||||
return 'underline';
|
||||
case UnderlineStyle.DOUBLE:
|
||||
@@ -727,6 +733,10 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
||||
const content: string[] = [];
|
||||
|
||||
if (attributesEquals(cell, oldCell)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
const flagsChanged = !equalFlags(cell, oldCell);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"outDir": "../out",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
@@ -23,6 +22,7 @@
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -203,6 +203,46 @@ test.describe('SerializeAddon', () => {
|
||||
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
test('buffer cell attributesEquals compares underline style and color', async () => {
|
||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
|
||||
const sameAttributes = await ctx.page.evaluate(`(() => {
|
||||
const line = window.term.buffer.active.getLine(0);
|
||||
const cellA = line?.getCell(0);
|
||||
const cellB = line?.getCell(1);
|
||||
if (!cellA || !cellB) {
|
||||
return undefined;
|
||||
}
|
||||
return cellA.attributesEquals(cellB);
|
||||
})()`);
|
||||
strictEqual(sameAttributes, true);
|
||||
|
||||
await ctx.page.evaluate(`window.term.reset()`);
|
||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_GREEN)}B${sgr(NORMAL)}`);
|
||||
const differentColor = await ctx.page.evaluate(`(() => {
|
||||
const line = window.term.buffer.active.getLine(0);
|
||||
const cellA = line?.getCell(0);
|
||||
const cellB = line?.getCell(1);
|
||||
if (!cellA || !cellB) {
|
||||
return undefined;
|
||||
}
|
||||
return cellA.attributesEquals(cellB);
|
||||
})()`);
|
||||
strictEqual(differentColor, false);
|
||||
|
||||
await ctx.page.evaluate(`window.term.reset()`);
|
||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINED, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
|
||||
const differentStyle = await ctx.page.evaluate(`(() => {
|
||||
const line = window.term.buffer.active.getLine(0);
|
||||
const cellA = line?.getCell(0);
|
||||
const cellB = line?.getCell(1);
|
||||
if (!cellA || !cellB) {
|
||||
return undefined;
|
||||
}
|
||||
return cellA.attributesEquals(cellB);
|
||||
})()`);
|
||||
strictEqual(differentStyle, false);
|
||||
});
|
||||
|
||||
test('serialize all rows of content with color256', async function(): Promise<any> {
|
||||
const rows = 32;
|
||||
const cols = 10;
|
||||
@@ -602,6 +642,9 @@ const BOLD = '1';
|
||||
const DIM = '2';
|
||||
const ITALIC = '3';
|
||||
const UNDERLINED = '4';
|
||||
const UNDERLINE_DOUBLE = '4:2';
|
||||
const UNDERLINE_COLOR_RED = '58;5;196';
|
||||
const UNDERLINE_COLOR_GREEN = '58;5;46';
|
||||
const BLINK = '5';
|
||||
const INVERSE = '7';
|
||||
const INVISIBLE = '8';
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "es6"],
|
||||
"rootDir": "..",
|
||||
"outDir": "../out-benchmark",
|
||||
"types": ["../../../node_modules/@types/node"],
|
||||
"moduleResolution": "node",
|
||||
"strict": false,
|
||||
"target": "es2015",
|
||||
"module": "commonjs",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -9,19 +9,15 @@ import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
|
||||
import type { UnicodeGraphemesAddon as IUnicodeGraphemesApi } from '@xterm/addon-unicode-graphemes';
|
||||
import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider';
|
||||
|
||||
export class UnicodeGraphemesAddon implements ITerminalAddon , IUnicodeGraphemesApi {
|
||||
export class UnicodeGraphemesAddon implements ITerminalAddon, IUnicodeGraphemesApi {
|
||||
private _provider15Graphemes?: UnicodeGraphemeProvider;
|
||||
private _provider15?: UnicodeGraphemeProvider;
|
||||
private _unicode?: IUnicodeHandling;
|
||||
private _oldVersion: string = '';
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
if (! this._provider15) {
|
||||
this._provider15 = new UnicodeGraphemeProvider(false);
|
||||
}
|
||||
if (! this._provider15Graphemes) {
|
||||
this._provider15Graphemes = new UnicodeGraphemeProvider(true);
|
||||
}
|
||||
this._provider15 ??= new UnicodeGraphemeProvider(false);
|
||||
this._provider15Graphemes ??= new UnicodeGraphemeProvider(true);
|
||||
const unicode = terminal.unicode;
|
||||
this._unicode = unicode;
|
||||
unicode.register(this._provider15);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import UnicodeTrie from './unicode-trie';
|
||||
const trieRaw = "AAARAAAAAABwxwAAAb4LQfTtmw+sVmUdx58LL/ffe/kjzNBV80gW1F3yR+6CvbJiypoZa0paWmAWSluErSBbFtYkkuZykq6QamGJ4WRqo2kFGy6dYWtEq6G1MFAJbRbOVTQr+x7f5+x97q/n/3me87wXzm/3s+f/7/d7/p7znnvOlvGMbQM7wIPgEbAPHABPgcPgefAS+BfYwuv/F/Q2OulBxKcK6TMRPxu8FcwFbwcjYCFYDC4Cl4ArwNXgGvBJsA58UdBDwy+jbBO4La8DtoEd4H7wkNBuN+KPgn3gADgIngaHwFHwF/AyeAWMm4C+TGi3LdiJ/EnIex04A2RgFpgD5oKFYDG4CLwHXAo+IKSvAqt4/evA9bz9jWA6+Cq3dyvCP8HWNwX93wF38/ROcD94SCjP2+1B+BiPP4HwgOD/7xD/I08fRniMx48jPAFeBeuF+n29jE0G08FZvaPHYWZvh9mcEfAOjlhXx/qGfd2QvLO3zccmtMnzliC9lPt+GenD1nyMiK/LNf1cycs+gfAzPJ6vtxe4jhuQtx5sBLeA28G3eb3v8/Beif4HkPewxu5G6N/rMP4qfgEdvwZPgj+AZ8Cx3nYfxiE8Dk6AV0FfH/YEOB28AbwJDIPzQAtcAC4Gl/Z19F+J+NVCehWPr0b46b7RvixvdPg8yr7U10l/BfFN4La8DdgGdoAHwU/AI2AfOACeAofB8+AlcAKwfvyBKeCM/o7NrF9PXmdWv9/Ynot2I7ztIg8dF5I2a8i63CjZU+9Fm2Wcy4U4ZQVYyeOrwVoev57UuxHcJKRvFuJXgnU8/nUebtbYrKmpCUOx31P7UVNTU1NTU1NTU1OGLTz8Xr/77+W7+9vP0or0MxPMbXaizY8FW3sQ3wseB/t5/kGEh8DR/vbzwL8i/Af4Dy8fP8BYE0weaKenI/wV/DhrQG97JspngzlgLpgHzgPzwUhdVpfVZXVZXRa87HxwAVgQ4Pn5WEd85l5TUzOasvezFw/E3b/LoP9D4CpwrcTWWsGXNQOj748/G9k3G56d1KYxmbELwQbwKFiJvBM8nDWlHa5E+AOwCzwLzjkNeeB28NvTeB1OYyr0gQ1g99R23nGE50xj7MPgc+A+8K5Bxj4FHgB/G2z/T9XEzCZjd/S0WYX4Pc3/r/Nn5I0f6qQXIP5x8ENwBMyYyNhHJ3b0pOCuLrBvM941NTU1JyNHEp+BrC8dMyalt1/m3uWfhmeULzRGp9d3wf0WZSN8+prCr60Wz09tuNmx35sl9Y825HXvRN39KNveaL8flb9f913kbec67kHeTsR3gYcH2uV7ED4m2HhCYi/X9ZuBzvuXv0f8iKIfx5B/XCg7gTgbVPdvAsomCuWnD45eK28UyvL3Jt+s0fU2TVnOXJQvJHUWIb0ELAWXgCt4+UcMumSsEtpch/g6ouMGpG/ieZsc9N/q4YsLd3D9WyPbsWEbfNgO7hN82TWY/n8xKbmsC3xQsYKf+7sjrx2TH+u4H3vhx+OO6+X9hmtXN7C/4r15EPaeBs9J7L7YBeeED/k7wn8fbIf/Rji+yVizmd4vW6bB19cb/PU9w7MxMA60bzPHgM8+zG623+OnzOf55yNc3Gw/k303wveBy3nZcoTXgNVgLfiCRNcG5N3SbIebwZ08fhe4l8d/BH7K4yI/4+HPwS/BAfBks+PzIaHuc3x+ivSL4GUyZ68I6fwZYRNMG2qnz+Th2QjfMtTx/1zE5w61nyN+Q7C3aKgdin1dgrylYBn4INdhGn/Z2FfFiqH01/SUXMvnPD+jC+j85N/RqRhR/DYaS6T+P09K1mD+vzW+5zVqqeVUl0wTz2lK8odJHRGXfBufdGLSoSo3+ZFJ6sl0qvJVNmhI4z4i06mrZ6uT1le1z5h5HE3tMiHPtQ5javu+ItMXUr/MXpmwmyRL3D6U7UwIMyYfczGu0qdqb2pbhcw4xQkhWQBMerrZ/liXrGTbsQwTwrEu4zSczKLrd7fCSKiKn+zSo8BWXMe8myXWOivrUxWi60OPoQ7VIasbQ0S/Ukk3rZVullNhHEL1rYoxUF0PTfm6elWJzq54ZsU4z11ohOy0oxT2izFqCNj4TesXcWZo6+Jfqr1O+1O1beqDagypj2J9F1u2daucj3Eknmq/6PaHrK7Mb1o35DiW1a/a76LuhlDXZX25SOz11S33ErKxDb2/fc/bFKI6axskn+4/W90u9mOtbRf7smsoTdvOfwoRz0t6DaP9k81v6P7Re5aUQudTd303rX+bZzBl97/KR7E+Xbux9lLI+aNr1PfaYLpPDiW2/vrYTX1drMIeXbMye6HXlw8292Jl7ZXxLxRlxXbcaH9drjFlxfa3Qozx8NWRi834lPVZbD+SmN7EJPzc9TVCSVXXDps9L+513b2J7fMu176V2YOhx1A3JrJ8KrLxUumpcu5j/lYT+2tzLRVDZmhjO442a1Clu0ox9VPVXzE/lcS4V0k1D6LI1pJsz8fct9SGbO5l/rmKzTlvsxdj3IvRtC2uv0t1fotltvd2VaCy5Sp5m0EhnZG4CCNxXZrWp/VUIrOjapfnNw11ZNI0V/GWzKNuxtzGKKTEtJeR0NVmpojbtBuW5On0u0is9ZMxvU8ZM+8vEyadtu10oqtP9Q4rcJEm85+Two/QkpGwjI6YkgkhtUfzZOW6fFVexuRri+qj9TJJHZkdmW5abiu0rs6uj2TMfmx06bISUj9tZ9Lja8dVQtox6WpxTJKfW3M4MSTmvU4sWy1CU6BF4jIfdNeDjHWuO1lCWIm2Jr2ixNZvklD2fP0Q6+vsmO4hqN1hJvfDtV5G8mTlsvau4qPP1a64L1skT6QYEzEtq0PzGZOfCbSdSmcKTP7Qs86Ej/1hEpelaV6IMdT5ayu2+nT9tmnnO746XbLxE8t0qOrYtJWhmk9bvaLfsrotRVw1PnR+bcafSUKZ6Mps7smobybJLH2R6WqRkJa1DHV0UmbfUcksiSF0HExSpp+uY0zbTklMaCm7blzEtg8h1rNMXNaYi05ZXsbC75sQ/4+aUxFV2jL50Q3jE0rK2rVtN09By8OHoo1vH2LPSdE323mr2sdu0pUZiDkWLRKWnfeQY6taKzHF9n/GPv8jd/0/egiRvYMR24fU79iY3s9Qva9RlYR8n8HHtq9fMcT1HRWfdZXiHd9YInt/iI4PTaf+BimXKvdXYU+3hlRpHzs2dVK/cxhDn+xs0I2jzxjL5kpXz1VU72aLtkK/97sALKyQqu25SshvG6h08/cLrlKswRklKXvvXfa+pZt+y8nah5YUv2Oo/ap/X2URdRfico9K69hcp6r6XaCz5Wo/hs/iNTGF6N6tV92/9ZS0Wba9SlT3pKF/e6W674+x9ly+VRL73cPU8ygb31D3eSqfVd+iqET0y3YMYojoO11XqrTt2nPxmeq1HYeqxkmUMt8DiesjpoTSr+qDrD+qPZDiOZxMdH0pRPX8MFUfQtv0Xbs+a1a1NnRryNZ/2+tsaPG5ZoX0RXZei88yZGdo4UMPj/cwv/kMJboxLISuQbE+1VW12Mx7FWOrW3M9Hv7Y+uxyraPSo8B2TGPuLdOeZha+hBKf8Sjsm/oR+7pmsx/oeOraFWdXleeV6oyl41zm+mgSuq9C6ox1TsU8D+m4dwMmf8v2nz7Tm+fYfj7HV1K/x1HWjquvY+2dllxM64ue87Su772zzbXIVC+WxLZTRR9MdkMTypZNH1z6G0tUvoccwxA+hfLNdV+a7MaQqscztMi+7QnxDZXvd1dldWQOyMbApb1Jd2h91Ffx+y9Xfb7tClokboOvrRhrbVpFFO8z+65t2/u4su9MUx028znH01/TGVDmHAj13W1o+1USw+eUfYtpO+b82rRNsb6oPpV+1fdBqddB6n3WDXvdJDZrJ0QfQp6bsc/kqq4BIddHWXGdN1pmWveh58F1zYUW1zmOITHXWOg1XrZvZSWUf77tq1ofqear6muaT1lIQp3bofabSafJVlnfYo9B6LGr8uzz2Xchvzfw+T9PlgiV/A8=";
|
||||
|
||||
declare const Buffer: any;
|
||||
declare const Buffer: { from(s: string, encoding: string): Uint8Array } | undefined;
|
||||
function _dec(s: string): Uint8Array {
|
||||
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'base64');
|
||||
const bs = atob(s);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||
import type { Unicode11Addon as IUnicode11Api } from '@xterm/addon-unicode11';
|
||||
import { UnicodeV11 } from './UnicodeV11';
|
||||
|
||||
export class Unicode11Addon implements ITerminalAddon , IUnicode11Api {
|
||||
export class Unicode11Addon implements ITerminalAddon, IUnicode11Api {
|
||||
public activate(terminal: Terminal): void {
|
||||
terminal.unicode.register(new UnicodeV11());
|
||||
}
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"@xterm/addon-unicode11": [
|
||||
"../typings/addon-unicode11.d.ts"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"types": [
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -35,7 +35,7 @@ function handleLink(event: MouseEvent, uri: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export class WebLinksAddon implements ITerminalAddon , IWebLinksApi {
|
||||
export class WebLinksAddon implements ITerminalAddon, IWebLinksApi {
|
||||
private _terminal: Terminal | undefined;
|
||||
private _linkProvider: IDisposable | undefined;
|
||||
|
||||
@@ -48,7 +48,7 @@ export class WebLinksAddon implements ITerminalAddon , IWebLinksApi {
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
const options = this._options as ILinkProviderOptions;
|
||||
const regex = options.urlRegex || strictUrlRegex;
|
||||
const regex = options.urlRegex ?? strictUrlRegex;
|
||||
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ICellData } from 'common/Types';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { rgba } from 'common/Color';
|
||||
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
||||
import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions';
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $fg = 0;
|
||||
@@ -42,7 +43,7 @@ export class CellColorResolver {
|
||||
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
|
||||
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
|
||||
*/
|
||||
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number): void {
|
||||
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number, deviceCellHeight: number): void {
|
||||
this.result.bg = cell.bg;
|
||||
this.result.fg = cell.fg;
|
||||
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
|
||||
@@ -63,7 +64,9 @@ export class CellColorResolver {
|
||||
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
|
||||
$variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
|
||||
}
|
||||
|
||||
if ($variantOffset === 0 && blockPatternCodepoints.has(code)) {
|
||||
$variantOffset = ((x * deviceCellWidth) % 2) * 2 + ((y * deviceCellHeight) % 2);
|
||||
}
|
||||
// Apply decorations on the bottom layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ITerminalOptions, Terminal } from '@xterm/xterm';
|
||||
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import { ICharAtlasConfig, ITextureAtlas } from './Types';
|
||||
import { generateConfig, configEquals } from './CharAtlasUtils';
|
||||
import type { ILogService } from 'common/services/Services';
|
||||
|
||||
interface ITextureAtlasCacheEntry {
|
||||
atlas: ITextureAtlas;
|
||||
@@ -67,8 +68,9 @@ export function acquireTextureAtlas(
|
||||
}
|
||||
|
||||
const core: ITerminal = (terminal as any)._core;
|
||||
const logService = (core as any)._logService as ILogService;
|
||||
const newEntry: ITextureAtlasCacheEntry = {
|
||||
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
|
||||
atlas: new TextureAtlas(document, newConfig, core.unicodeService, logService),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { toDisposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { toDisposable, IDisposable } from 'common/Lifecycle';
|
||||
|
||||
export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable {
|
||||
// Observe any resizes to the element and extract the actual pixel size of the element if the
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { TextureAtlas } from './TextureAtlas';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types';
|
||||
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { IThemeService } from 'browser/services/Services';
|
||||
import { ReadonlyColorSet } from 'browser/Types';
|
||||
import { Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IColor } from 'common/Types';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
|
||||
@@ -14,8 +14,8 @@ import { IdleTaskQueue } from 'common/TaskQueue';
|
||||
import { IColor } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { IUnicodeService } from 'common/services/Services';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { ILogService, IUnicodeService } from 'common/services/Services';
|
||||
import { Emitter } from 'common/Event';
|
||||
|
||||
/**
|
||||
* A shared object which is used to draw nothing for a particular cell.
|
||||
@@ -88,7 +88,8 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
constructor(
|
||||
private readonly _document: Document,
|
||||
private readonly _config: ICharAtlasConfig,
|
||||
private readonly _unicodeService: IUnicodeService
|
||||
private readonly _unicodeService: IUnicodeService,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
this._createNewPage();
|
||||
this._tmpCanvas = createCanvas(
|
||||
@@ -119,7 +120,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
|
||||
private _doWarmUp(): void {
|
||||
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
|
||||
const queue = new IdleTaskQueue();
|
||||
const queue = new IdleTaskQueue(this._logService);
|
||||
for (let i = 33; i < 126; i++) {
|
||||
queue.enqueue(() => {
|
||||
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
|
||||
@@ -176,6 +177,17 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
|
||||
// Gather details of the merge
|
||||
const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
|
||||
|
||||
// Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
|
||||
// effectively reduce page count and merging would cause issues.
|
||||
if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
|
||||
const newPage = new AtlasPage(this._document, this._textureSize);
|
||||
this._pages.push(newPage);
|
||||
this._activePages.push(newPage);
|
||||
this._onAddTextureAtlasCanvas.fire(newPage.canvas);
|
||||
return newPage;
|
||||
}
|
||||
|
||||
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
|
||||
const mergedPageIndex = this.pages.length - mergingPages.length;
|
||||
|
||||
@@ -399,7 +411,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
const cache = this._getContrastCache(dim);
|
||||
const adjustedColor = cache.getColor(bg, fg);
|
||||
if (adjustedColor !== undefined) {
|
||||
return adjustedColor || undefined;
|
||||
return adjustedColor ?? undefined;
|
||||
}
|
||||
|
||||
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
|
||||
@@ -514,7 +526,8 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
// Draw custom characters if applicable
|
||||
let customGlyph = false;
|
||||
if (this._config.customGlyphs !== false) {
|
||||
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css);
|
||||
const variantOffset = this._workAttributeData.getUnderlineVariantOffset();
|
||||
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset);
|
||||
}
|
||||
|
||||
// Whether to clear pixels based on a threshold difference between the glyph color and the
|
||||
|
||||
@@ -7,7 +7,7 @@ import { FontWeight } from '@xterm/xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types';
|
||||
import type { Event } from 'vs/base/common/event';
|
||||
import type { IEvent } from 'common/Event';
|
||||
|
||||
export interface IRenderModel {
|
||||
cells: Uint32Array;
|
||||
@@ -58,8 +58,8 @@ export interface ICharAtlasConfig {
|
||||
export interface ITextureAtlas extends IDisposable {
|
||||
readonly pages: { canvas: HTMLCanvasElement, version: number }[];
|
||||
|
||||
onAddTextureAtlasCanvas: Event<HTMLCanvasElement>;
|
||||
onRemoveTextureAtlasCanvas: Event<HTMLCanvasElement>;
|
||||
onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
|
||||
|
||||
@@ -7,15 +7,14 @@ import type { ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||
import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { getSafariVersion, isSafari } from 'common/Platform';
|
||||
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { setTraceLogger } from 'common/services/LogService';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Emitter, EventUtils } from 'common/Event';
|
||||
|
||||
export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi {
|
||||
export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi {
|
||||
private _terminal?: Terminal;
|
||||
private _renderer?: WebglRenderer;
|
||||
|
||||
@@ -66,13 +65,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
|
||||
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
|
||||
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
|
||||
const decorationService: IDecorationService = unsafeCore._decorationService;
|
||||
const logService: ILogService = unsafeCore._logService;
|
||||
const themeService: IThemeService = unsafeCore._themeService;
|
||||
|
||||
// Set trace logger just in case it hasn't been yet which could happen when the addon is
|
||||
// bundled separately to the core module
|
||||
setTraceLogger(logService);
|
||||
|
||||
this._renderer = this._register(new WebglRenderer(
|
||||
terminal,
|
||||
characterJoinerService,
|
||||
@@ -85,10 +79,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
|
||||
this._customGlyphs,
|
||||
this._preserveDrawingBuffer
|
||||
));
|
||||
this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss));
|
||||
this._register(Event.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||
this._register(Event.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||
this._register(Event.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
|
||||
this._register(EventUtils.forward(this._renderer.onContextLoss, this._onContextLoss));
|
||||
this._register(EventUtils.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||
this._register(EventUtils.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||
this._register(EventUtils.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
|
||||
renderService.setRenderer(this._renderer);
|
||||
|
||||
this._register(toDisposable(() => {
|
||||
|
||||
@@ -13,7 +13,8 @@ import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeS
|
||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
@@ -22,14 +23,15 @@ import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET
|
||||
import { IWebGL2RenderingContext, type ITextureAtlas } from './Types';
|
||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, EventUtils } from 'common/Event';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
|
||||
private _textBlinkStateManager: TextBlinkStateManager;
|
||||
private _charAtlasDisposable = this._register(new MutableDisposable());
|
||||
private _charAtlas: ITextureAtlas | undefined;
|
||||
private _devicePixelRatio: number;
|
||||
@@ -37,8 +39,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _observerDisposable = this._register(new MutableDisposable());
|
||||
|
||||
private _model: RenderModel = new RenderModel();
|
||||
private _rowHasBlinkingCells: boolean[] = [];
|
||||
private _rowHasBlinkingCellsCount: number = 0;
|
||||
private _workCell: ICellData = new CellData();
|
||||
private _workCell2: ICellData = new CellData();
|
||||
private _cellColorResolver: CellColorResolver;
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
@@ -105,6 +108,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._updateDimensions();
|
||||
this._updateCursorBlink();
|
||||
this._register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this._textBlinkStateManager = this._register(new TextBlinkStateManager(
|
||||
() => this._requestRedrawViewport(),
|
||||
this._coreBrowserService,
|
||||
this._optionsService
|
||||
));
|
||||
this._resetBlinkingRowState();
|
||||
|
||||
this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);
|
||||
|
||||
@@ -178,6 +187,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._updateDimensions();
|
||||
|
||||
this._model.resize(this._terminal.cols, this._terminal.rows);
|
||||
this._resetBlinkingRowState();
|
||||
|
||||
// Resize all render layers
|
||||
for (const l of this._renderLayers) {
|
||||
@@ -231,6 +241,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public handleViewportVisibilityChange(isVisible: boolean): void {
|
||||
this._textBlinkStateManager.setViewportVisible(isVisible);
|
||||
}
|
||||
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||
@@ -290,8 +304,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
if (this._charAtlas !== atlas) {
|
||||
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
||||
this._charAtlasDisposable.value = combinedDisposable(
|
||||
Event.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
|
||||
Event.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
|
||||
EventUtils.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
|
||||
EventUtils.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
|
||||
);
|
||||
}
|
||||
this._charAtlas = atlas;
|
||||
@@ -323,6 +337,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
l.reset(this._terminal);
|
||||
}
|
||||
|
||||
this._resetBlinkingRowState();
|
||||
this._textBlinkStateManager.setNeedsBlinkInViewport(false);
|
||||
|
||||
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
|
||||
this._updateCursorBlink();
|
||||
}
|
||||
@@ -420,6 +437,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
for (y = start; y <= end; y++) {
|
||||
row = y + terminal.buffer.ydisp;
|
||||
line = terminal.buffer.lines.get(row)!;
|
||||
let rowHasBlinkingCells = false;
|
||||
this._model.lineLengths[y] = 0;
|
||||
isCursorRow = cursorY === row;
|
||||
skipJoinedCheckUntilX = 0;
|
||||
@@ -477,8 +495,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
code = cell.getCode();
|
||||
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
|
||||
if (!rowHasBlinkingCells && cell.isBlink()) {
|
||||
rowHasBlinkingCells = true;
|
||||
}
|
||||
|
||||
// Load colors/resolve overrides into work colors
|
||||
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width);
|
||||
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width, this.dimensions.device.cell.height);
|
||||
|
||||
// Override colors for cursor cell
|
||||
if (isCursorVisible && row === cursorY) {
|
||||
@@ -506,6 +528,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._textBlinkStateManager.isEnabled && !this._textBlinkStateManager.isBlinkOn && cell.isBlink()) {
|
||||
this._cellColorResolver.result.fg |= FgFlags.INVISIBLE;
|
||||
}
|
||||
|
||||
if (code !== NULL_CELL_CODE) {
|
||||
this._model.lineLengths[y] = x + 1;
|
||||
}
|
||||
@@ -552,11 +578,31 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
x--; // Go back to the previous update cell for next iteration
|
||||
}
|
||||
}
|
||||
this._setRowBlinkState(y, rowHasBlinkingCells);
|
||||
}
|
||||
if (modelUpdated) {
|
||||
this._rectangleRenderer.value!.updateBackgrounds(this._model);
|
||||
}
|
||||
this._rectangleRenderer.value!.updateCursor(this._model);
|
||||
this._updateTextBlinkState();
|
||||
}
|
||||
|
||||
private _resetBlinkingRowState(): void {
|
||||
this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false);
|
||||
this._rowHasBlinkingCellsCount = 0;
|
||||
}
|
||||
|
||||
private _setRowBlinkState(row: number, hasBlinkingCells: boolean): void {
|
||||
const previous = this._rowHasBlinkingCells[row];
|
||||
if (previous === hasBlinkingCells) {
|
||||
return;
|
||||
}
|
||||
this._rowHasBlinkingCells[row] = hasBlinkingCells;
|
||||
this._rowHasBlinkingCellsCount += hasBlinkingCells ? 1 : -1;
|
||||
}
|
||||
|
||||
private _updateTextBlinkState(): void {
|
||||
this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,35 @@ import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType,
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
const enum Shapes {
|
||||
/** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1',
|
||||
/** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5',
|
||||
|
||||
/** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5',
|
||||
/** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5',
|
||||
/** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1',
|
||||
/** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5',
|
||||
|
||||
/** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0',
|
||||
/** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5',
|
||||
/** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5',
|
||||
/** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1',
|
||||
|
||||
/** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0',
|
||||
/** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5',
|
||||
/** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5',
|
||||
/** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1',
|
||||
|
||||
/** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1',
|
||||
|
||||
/** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled
|
||||
/** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled
|
||||
/** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled
|
||||
/** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9',
|
||||
/** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333',
|
||||
/** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95',
|
||||
}
|
||||
|
||||
namespace GitBranchSymbolsParts {
|
||||
// Lines
|
||||
export const LINE_H: CustomGlyphDefinitionPart = Object.freeze({ type: CustomGlyphDefinitionType.PATH_FUNCTION, data: Shapes.LEFT_TO_RIGHT, strokeWidth: 1 });
|
||||
@@ -644,8 +673,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
|
||||
] },
|
||||
|
||||
// Diagonal fill characters (1FB98-1FB99)
|
||||
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,0 L1,1 M0,.25 L.75,1 M0,.5 L.5,1 M0,.75 L.25,1 M.25,0 L1,.75 M.5,0 L1,.5 M.75,0 L1,.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
|
||||
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,.25 L.25,0 M0,.5 L.5,0 M0,.75 L.75,0 M0,1 L1,0 M.25,1 L1,.25 M.5,1 L1,.5 M.75,1 L1,.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
|
||||
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,-0.25 L1.25,1.25 M-0.25,0 L1,1.25 M-0.25,0.25 L0.75,1.25 M-0.25,0.5 L0.5,1.25 M0,-0.25 L1.25,1 M0.25,-0.25 L1.25,0.75 M0.5,-0.25 L1.25,0.5 M-0.25,0.75 L0.25,1.25 M0.75,-0.25 L1.25,0.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
|
||||
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,0.5 L0.5,-0.25 M-0.25,0.75 L0.75,-0.25 M-0.25,1 L1,-0.25 M-0.25,1.25 L1.25,-0.25 M0,1.25 L1.25,0 M0.25,1.25 L1.25,0.25 M0.5,1.25 L1.25,0.5 M-0.25,0.25 L0.25,-0.25 M0.75,1.25 L1.25,0.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
|
||||
|
||||
// Smooth mosaic terminal graphic characters (1FB9A-1FB9B)
|
||||
'\u{1FB9A}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: 'M0,0 L.5,.5 L0,1 L1,1 L.5,.5 L1,0', type: CustomGlyphVectorType.FILL } }, // UPPER AND LOWER TRIANGULAR HALF BLOCK
|
||||
@@ -820,6 +849,27 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
|
||||
// #endregion
|
||||
};
|
||||
|
||||
export const blockPatternCodepoints = new Set<number>([
|
||||
// Shade characters (2591-2593)
|
||||
0x2591,
|
||||
0x2592,
|
||||
0x2593,
|
||||
// Rectangular shade characters (1FB8C-1FB94)
|
||||
0x1FB8C,
|
||||
0x1FB8D,
|
||||
0x1FB8E,
|
||||
0x1FB8F,
|
||||
0x1FB90,
|
||||
0x1FB91,
|
||||
0x1FB92,
|
||||
0x1FB94,
|
||||
// Triangular shade characters (1FB9C-1FB9F)
|
||||
0x1FB9C,
|
||||
0x1FB9D,
|
||||
0x1FB9E,
|
||||
0x1FB9F
|
||||
]);
|
||||
|
||||
/**
|
||||
* Generates a drawing function for sextant characters. Sextants are a 2x3 grid where each cell
|
||||
* can be on or off.
|
||||
@@ -967,31 +1017,3 @@ function segmentedDigit(pattern: number): string {
|
||||
return paths.join(' ');
|
||||
}
|
||||
|
||||
const enum Shapes {
|
||||
/** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1',
|
||||
/** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5',
|
||||
|
||||
/** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5',
|
||||
/** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5',
|
||||
/** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1',
|
||||
/** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5',
|
||||
|
||||
/** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0',
|
||||
/** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5',
|
||||
/** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5',
|
||||
/** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1',
|
||||
|
||||
/** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0',
|
||||
/** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5',
|
||||
/** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5',
|
||||
/** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1',
|
||||
|
||||
/** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1',
|
||||
|
||||
/** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled
|
||||
/** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled
|
||||
/** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled
|
||||
/** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9',
|
||||
/** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333',
|
||||
/** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95',
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user