Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4997f529 | ||
|
|
16ea2ec0ef | ||
|
|
eb0f5383c3 | ||
|
|
a4502a6d00 | ||
|
|
c11a8f43f4 | ||
|
|
d7ba56cd28 | ||
|
|
be44833f12 | ||
|
|
fcb3cc1a21 | ||
|
|
376268157c | ||
|
|
5ceb02b9c6 | ||
|
|
d320898231 | ||
|
|
cf3939af4a | ||
|
|
596ab845d0 | ||
|
|
a59d527d1a | ||
|
|
08f85cf826 | ||
|
|
e95ec8d4bc | ||
|
|
d15b426bd4 | ||
|
|
c907edd75f | ||
|
|
1029e19b57 | ||
|
|
d44900faa2 | ||
|
|
5f724c5924 | ||
|
|
ae58176bcb | ||
|
|
429b2882f1 | ||
|
|
ffac8c03ab | ||
|
|
a7c4a160a8 | ||
|
|
c9dc994090 | ||
|
|
e4710d3286 | ||
|
|
8f9045ed88 | ||
|
|
b8e5ec7304 | ||
|
|
6a3d2480e0 | ||
|
|
735afde77f | ||
|
|
53efc95170 | ||
|
|
3512f140de | ||
|
|
dbe35428cb | ||
|
|
297ac838a3 | ||
|
|
5316cb0812 | ||
|
|
c124550971 | ||
|
|
6525a7e68e | ||
|
|
b2133d93f0 | ||
|
|
a5e4f90139 | ||
|
|
a27b8feb8f | ||
|
|
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,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).
|
||||
@@ -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'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 |
@@ -25,6 +25,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.1.0"
|
||||
"xterm-wasm-parts": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import Base64Decoder, { type DecodeStatus } 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
|
||||
// limit hold memory in base64 decoder (encoded bytes)
|
||||
const KEEP_DATA = 4194304;
|
||||
const INITIAL_DATA = 1048576;
|
||||
|
||||
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
||||
const DECODER_OK: DecodeStatus.OK = 0;
|
||||
|
||||
// default IIP header values
|
||||
const DEFAULT_HEADER: IHeaderFields = {
|
||||
@@ -31,7 +31,7 @@ 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(
|
||||
@@ -39,7 +39,11 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {}
|
||||
) {
|
||||
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
|
||||
const initialBytes = Math.min(INITIAL_DATA, maxEncodedBytes);
|
||||
this._dec = new Base64Decoder(KEEP_DATA, maxEncodedBytes, initialBytes);
|
||||
}
|
||||
|
||||
public reset(): void {}
|
||||
|
||||
@@ -54,7 +58,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)) !== DECODER_OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
@@ -70,8 +74,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)) !== DECODER_OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
@@ -89,13 +93,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 +111,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);
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 { SixelHandler } from './SixelHandler';
|
||||
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
||||
|
||||
@@ -22,7 +23,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 +51,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 +93,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;
|
||||
@@ -144,6 +147,15 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
||||
);
|
||||
}
|
||||
|
||||
// Kitty graphics handler
|
||||
if (this._opts.kittySupport) {
|
||||
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, this._storage!, terminal);
|
||||
this._handlers.set('kitty', kittyHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
||||
|
||||
@@ -38,7 +38,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;
|
||||
@@ -242,7 +242,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
}
|
||||
if (!this._placeholder) return;
|
||||
this._ctx.drawImage(
|
||||
this._placeholderBitmap || this._placeholder!,
|
||||
this._placeholderBitmap ?? this._placeholder!,
|
||||
col * width,
|
||||
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
||||
width * count,
|
||||
|
||||
@@ -132,8 +132,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 = {
|
||||
@@ -233,9 +235,22 @@ export class ImageStorage implements IDisposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add an image to the storage.
|
||||
* Delete an image by its internal storage ID.
|
||||
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
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.
|
||||
* Returns the internal image ID assigned to the stored image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): number {
|
||||
// never allow storage to exceed memory limit
|
||||
this._evictOldest(img.width * img.height);
|
||||
|
||||
@@ -329,6 +344,7 @@ export class ImageStorage implements IDisposable {
|
||||
|
||||
// finally add the image
|
||||
this._images.set(imageId, imgSpec);
|
||||
return imageId;
|
||||
}
|
||||
|
||||
|
||||
@@ -381,7 +397,7 @@ export class ImageStorage implements IDisposable {
|
||||
if (!line) return;
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
||||
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
|
||||
let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
@@ -400,7 +416,7 @@ export class ImageStorage implements IDisposable {
|
||||
while (
|
||||
++col < cols
|
||||
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
|
||||
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
|
||||
&& (e = line._extendedAttrs[col] ?? EMPTY_ATTRS)
|
||||
&& (e.imageId === imageId)
|
||||
&& (e.tileId === startTile + count)
|
||||
) {
|
||||
@@ -442,7 +458,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 +503,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 +523,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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IApcHandler, IImageAddonOptions, IResetHandler, ITerminalExt } from '../Types';
|
||||
import { ImageRenderer } from '../ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from '../ImageStorage';
|
||||
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 {
|
||||
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();
|
||||
private _nextImageId = 1;
|
||||
/** Maps Kitty protocol image ID → ImageStorage internal ID for deletion/lookup. */
|
||||
private _kittyIdToStorageId: Map<number, number> = new Map();
|
||||
// TODO: Eliminate double storage — raw image data lives here (as Blob) AND rendered
|
||||
// ImageBitmaps live in ImageStorage. Currently we only use ImageStorage.addImage(bitmap)
|
||||
// for tiling + cursor movement + marker-based eviction.
|
||||
//
|
||||
|
||||
// See: https://github.com/xtermjs/xterm.js/pull/5619#issuecomment-3853678815
|
||||
private _images: Map<number, IKittyImageData> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _storage: ImageStorage,
|
||||
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 {
|
||||
for (const pending of this._pendingTransmissions.values()) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
if (this._activeDecoder) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
this._images.clear();
|
||||
this._kittyIdToStorageId.clear();
|
||||
}
|
||||
|
||||
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
|
||||
const pendingKey = this._parsedCommand?.id ?? 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._pendingTransmissions.delete(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._pendingTransmissions.delete(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);
|
||||
}
|
||||
|
||||
const pendingKey = cmd.id ?? 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._activeDecoder = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
default:
|
||||
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);
|
||||
if (cmd.id !== undefined && !decodeError && 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);
|
||||
default:
|
||||
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)
|
||||
|
||||
if (decodeError || bytes.length === 0) return true;
|
||||
|
||||
const id = cmd.id ?? this._nextImageId++;
|
||||
const image: IKittyImageData = {
|
||||
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 ?? ''
|
||||
};
|
||||
this._images.set(id, image);
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleTransmitDisplay(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
|
||||
if (decodeError) return true;
|
||||
|
||||
const pendingKey = cmd.id ?? 0;
|
||||
|
||||
this._handleTransmit(cmd, bytes, decodeError);
|
||||
|
||||
// If still accumulating chunks, don't display yet
|
||||
if (this._pendingTransmissions.has(pendingKey)) return true;
|
||||
|
||||
// Display the completed image
|
||||
const id = cmd.id ?? this._nextImageId - 1;
|
||||
const image = this._images.get(id);
|
||||
if (image) {
|
||||
const result = this._displayImage(image, cmd);
|
||||
if (cmd.id !== undefined) {
|
||||
return (result as Promise<boolean>).then(r => {
|
||||
this._sendResponse(id, 'OK', cmd.quiet ?? 0);
|
||||
return r;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleQuery(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
|
||||
const id = cmd.id ?? 0;
|
||||
const quiet = cmd.quiet ?? 0;
|
||||
|
||||
// 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 {
|
||||
const id = cmd.id;
|
||||
|
||||
if (id !== undefined) {
|
||||
// Abort in-flight chunked upload for this specific image
|
||||
const pending = this._pendingTransmissions.get(id);
|
||||
if (pending) {
|
||||
pending.decoder.release();
|
||||
this._pendingTransmissions.delete(id);
|
||||
}
|
||||
|
||||
this._images.delete(id);
|
||||
const storageId = this._kittyIdToStorageId.get(id);
|
||||
if (storageId !== undefined) {
|
||||
this._storage.deleteImage(storageId);
|
||||
this._kittyIdToStorageId.delete(id);
|
||||
}
|
||||
} else {
|
||||
// Abort all in-flight chunked uploads
|
||||
for (const pending of this._pendingTransmissions.values()) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
|
||||
this._images.clear();
|
||||
for (const storageId of this._kittyIdToStorageId.values()) {
|
||||
this._storage.deleteImage(storageId);
|
||||
}
|
||||
this._kittyIdToStorageId.clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _sendResponse(id: number, message: string, quiet: number): void {
|
||||
const isOk = message === 'OK';
|
||||
if (isOk && quiet === 1) return;
|
||||
if (!isOk && quiet === 2) return;
|
||||
|
||||
const response = `\x1b_Gi=${id};${message}\x1b\\`;
|
||||
this._coreTerminal._core.coreService.triggerDataEvent(response);
|
||||
}
|
||||
|
||||
// Image display
|
||||
|
||||
private _displayImage(image: IKittyImageData, cmd: IKittyCommand): boolean | Promise<boolean> {
|
||||
return this._decodeAndDisplay(image, cmd)
|
||||
.then(() => true)
|
||||
.catch(() => true);
|
||||
}
|
||||
|
||||
private async _decodeAndDisplay(image: IKittyImageData, cmd: IKittyCommand): Promise<void> {
|
||||
const bitmap = await this._createBitmap(image);
|
||||
|
||||
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
|
||||
const imgCols = cmd.columns ?? Math.ceil(bitmap.width / cw);
|
||||
const imgRows = cmd.rows ?? 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) return;
|
||||
|
||||
// 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;
|
||||
|
||||
let storageId: number;
|
||||
if (w !== bitmap.width || h !== bitmap.height) {
|
||||
const resized = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
|
||||
storageId = this._storage.addImage(resized);
|
||||
} else {
|
||||
storageId = this._storage.addImage(bitmap);
|
||||
}
|
||||
this._kittyIdToStorageId.set(image.id, storageId);
|
||||
|
||||
// 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', reject);
|
||||
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;
|
||||
const data = new Uint8ClampedArray(pixelCount * BYTES_PER_PIXEL_RGBA);
|
||||
const isRgba = image.format === KittyFormat.RGBA;
|
||||
|
||||
let srcOffset = 0;
|
||||
let dstOffset = 0;
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
data[dstOffset] = bytes[srcOffset];
|
||||
data[dstOffset + 1] = bytes[srcOffset + 1];
|
||||
data[dstOffset + 2] = bytes[srcOffset + 2];
|
||||
data[dstOffset + 3] = isRgba ? bytes[srcOffset + 3] : ALPHA_OPAQUE;
|
||||
srcOffset += bytesPerPixel;
|
||||
dstOffset += 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._images;
|
||||
}
|
||||
|
||||
public get pendingTransmissions(): ReadonlyMap<number, IPendingTransmission> {
|
||||
return this._pendingTransmissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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',
|
||||
// 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'
|
||||
}
|
||||
|
||||
// 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;
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
more?: number;
|
||||
quiet?: number;
|
||||
cursorMovement?: 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;
|
||||
}
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
|
||||
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
|
||||
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } 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';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -457,7 +457,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 +601,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 +630,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 +666,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;
|
||||
@@ -700,7 +686,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('');
|
||||
}
|
||||
// Palette color
|
||||
return this._ansiColors[color].css;
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -15,7 +15,7 @@ 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 { Emitter } from 'common/Event';
|
||||
|
||||
/**
|
||||
* A shared object which is used to draw nothing for a particular cell.
|
||||
@@ -176,6 +176,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 +410,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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -22,9 +22,9 @@ 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 { Emitter, EventUtils } from 'common/Event';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
@@ -290,8 +290,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;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ReadonlyColorSet } from 'browser/Types';
|
||||
import { acquireTextureAtlas } from '../CharAtlasCache';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
|
||||
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ const addons = [
|
||||
'clipboard',
|
||||
'fit',
|
||||
'image',
|
||||
'kitty-graphics',
|
||||
'progress',
|
||||
'search',
|
||||
'serialize',
|
||||
|
||||
+104
-82
@@ -16,9 +16,12 @@ if ('WebAssembly' in window) {
|
||||
import { Terminal, ITerminalOptions, type ITheme } from '@xterm/xterm';
|
||||
import { AttachAddon } from '@xterm/addon-attach';
|
||||
import { AddonImageWindow } from './components/window/addonImageWindow';
|
||||
import { AddonLigaturesWindow } from './components/window/addonLigaturesWindow';
|
||||
import { AddonProgressWindow } from './components/window/addonProgressWindow';
|
||||
import { AddonSearchWindow } from './components/window/addonSearchWindow';
|
||||
import { AddonSerializeWindow } from './components/window/addonSerializeWindow';
|
||||
import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow';
|
||||
import { AddonWebLinksWindow } from './components/window/addonWebLinksWindow';
|
||||
import { AddonsWindow } from './components/window/addonsWindow';
|
||||
import { CellInspectorWindow } from './components/window/cellInspectorWindow';
|
||||
import { ControlBar } from './components/controlBar';
|
||||
@@ -58,11 +61,11 @@ export interface IWindowWithTerminal extends Window {
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
|
||||
let term;
|
||||
let protocol;
|
||||
let socketURL;
|
||||
let socket;
|
||||
let pid;
|
||||
let term: Terminal | null;
|
||||
let protocol: string;
|
||||
let socketURL: string;
|
||||
let socket: WebSocket | null;
|
||||
let pid: string;
|
||||
let controlBar: ControlBar;
|
||||
let addonsWindow: AddonsWindow;
|
||||
let addonSearchWindow: AddonSearchWindow;
|
||||
@@ -73,7 +76,7 @@ const addons: AddonCollection = {
|
||||
attach: { name: 'attach', ctor: AttachAddon, canChange: false },
|
||||
clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true },
|
||||
fit: { name: 'fit', ctor: FitAddon, canChange: false },
|
||||
image: { name: 'image', ctor: ImageAddon, canChange: true },
|
||||
image: { name: 'image', ctor: ImageAddon!, canChange: true },
|
||||
progress: { name: 'progress', ctor: ProgressAddon, canChange: true },
|
||||
search: { name: 'search', ctor: SearchAddon, canChange: true },
|
||||
serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
|
||||
@@ -116,8 +119,8 @@ const xtermjsTheme = {
|
||||
brightWhite: '#FFFFFF'
|
||||
} satisfies ITheme;
|
||||
function setPadding(): void {
|
||||
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
||||
addons.fit.instance.fit();
|
||||
term!.element!.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
||||
addons.fit.instance!.fit();
|
||||
}
|
||||
|
||||
function getSearchOptions(): ISearchOptions {
|
||||
@@ -141,7 +144,7 @@ const disposeRecreateButtonHandler: () => void = () => {
|
||||
if (term) {
|
||||
term.dispose();
|
||||
term = null;
|
||||
window.term = null;
|
||||
(window as any).term = null;
|
||||
socket = null;
|
||||
addons.attach.instance = undefined;
|
||||
addons.clipboard.instance = undefined;
|
||||
@@ -154,10 +157,10 @@ const disposeRecreateButtonHandler: () => void = () => {
|
||||
addons.ligatures.instance = undefined;
|
||||
addons.webLinks.instance = undefined;
|
||||
addons.webgl.instance = undefined;
|
||||
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
|
||||
document.getElementById('dispose')!.innerHTML = 'Recreate Terminal';
|
||||
} else {
|
||||
createTerminal();
|
||||
document.getElementById('dispose').innerHTML = 'Dispose terminal';
|
||||
document.getElementById('dispose')!.innerHTML = 'Dispose terminal';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,7 +168,7 @@ const createNewWindowButtonHandler: () => void = () => {
|
||||
if (term) {
|
||||
disposeRecreateButtonHandler();
|
||||
}
|
||||
const win = window.open();
|
||||
const win = window.open()!;
|
||||
terminalContainer = win.document.createElement('div');
|
||||
terminalContainer.id = 'terminal-container';
|
||||
win.document.body.appendChild(terminalContainer);
|
||||
@@ -207,17 +210,20 @@ if (document.location.pathname === '/test') {
|
||||
} else {
|
||||
const typedTerm = createTerminal();
|
||||
|
||||
controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), []);
|
||||
controlBar = new ControlBar(document.getElementById('sidebar')!, document.querySelector('.banner-tabs')!, []);
|
||||
optionsWindow = controlBar.registerWindow(new OptionsWindow(typedTerm, addons, { updateTerminalSize, updateTerminalContainerBackground }));
|
||||
const styleWindow = controlBar.registerWindow(new StyleWindow(typedTerm, addons));
|
||||
controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons));
|
||||
controlBar.registerWindow(new VtWindow(typedTerm, addons));
|
||||
addonsWindow = controlBar.registerWindow(new AddonsWindow(typedTerm, addons));
|
||||
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonLigaturesWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonProgressWindow(typedTerm, addons), { afterId: 'addon-ligatures', hidden: true, italics: true });
|
||||
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addon-progress', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
|
||||
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonWebLinksWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
|
||||
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-links', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
|
||||
actionElements = {
|
||||
findNext: addonSearchWindow.findNextInput,
|
||||
@@ -229,48 +235,51 @@ if (document.location.pathname === '/test') {
|
||||
// TODO: Most of below should be encapsulated within windows
|
||||
paddingElement = styleWindow.paddingElement;
|
||||
|
||||
controlBar.setTabVisible('addon-webgl', true);
|
||||
controlBar.setTabVisible('addon-image', !!addons.image.instance);
|
||||
controlBar.setTabVisible('addon-ligatures', !!addons.ligatures.instance);
|
||||
controlBar.setTabVisible('addon-progress', !!addons.progress.instance);
|
||||
controlBar.setTabVisible('addon-search', true);
|
||||
controlBar.setTabVisible('addon-serialize', true);
|
||||
controlBar.setTabVisible('addon-image', true);
|
||||
controlBar.setTabVisible('addon-web-fonts', true);
|
||||
addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
||||
addons.webgl.instance.onRemoveTextureAtlasCanvas(e => addonWebglWindow.removeTextureAtlas(e));
|
||||
controlBar.setTabVisible('addon-web-links', !!addons.webLinks.instance);
|
||||
controlBar.setTabVisible('addon-webgl', true);
|
||||
addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
|
||||
addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
||||
addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
||||
addons.webgl.instance!.onRemoveTextureAtlasCanvas(e => addonWebglWindow.removeTextureAtlas(e));
|
||||
|
||||
paddingElement.value = '0';
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
addDomListener(actionElements.findNext, 'keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
|
||||
addons.search.instance!.findNext(actionElements.findNext.value, getSearchOptions());
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
addDomListener(actionElements.findNext, 'input', (e) => {
|
||||
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
|
||||
addons.search.instance!.findNext(actionElements.findNext.value, getSearchOptions());
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
addons.search.instance!.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'input', (e) => {
|
||||
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
addons.search.instance!.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
});
|
||||
addDomListener(actionElements.findNext, 'blur', (e) => {
|
||||
addons.search.instance.clearActiveDecoration();
|
||||
addons.search.instance!.clearActiveDecoration();
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'blur', (e) => {
|
||||
addons.search.instance.clearActiveDecoration();
|
||||
addons.search.instance!.clearActiveDecoration();
|
||||
});
|
||||
}
|
||||
|
||||
function createTerminal(): Terminal {
|
||||
// Clean terminal
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
while (terminalContainer!.children.length) {
|
||||
terminalContainer!.removeChild(terminalContainer!.children[0]);
|
||||
}
|
||||
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
@@ -282,10 +291,7 @@ function createTerminal(): Terminal {
|
||||
buildNumber: 22621
|
||||
} : undefined,
|
||||
fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"',
|
||||
theme: { ...xtermjsTheme },
|
||||
vtExtensions: {
|
||||
win32InputMode: isWindows
|
||||
}
|
||||
theme: { ...xtermjsTheme }
|
||||
} as ITerminalOptions);
|
||||
|
||||
// Load addons
|
||||
@@ -293,7 +299,7 @@ function createTerminal(): Terminal {
|
||||
addons.search.instance = new SearchAddon();
|
||||
addons.serialize.instance = new SerializeAddon();
|
||||
addons.fit.instance = new FitAddon();
|
||||
addons.image.instance = new ImageAddon();
|
||||
addons.image.instance = new ImageAddon!();
|
||||
addons.progress.instance = new ProgressAddon();
|
||||
addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon();
|
||||
addons.clipboard.instance = new ClipboardAddon();
|
||||
@@ -314,14 +320,16 @@ function createTerminal(): Terminal {
|
||||
typedTerm.loadAddon(addons.webFonts.instance);
|
||||
typedTerm.loadAddon(addons.clipboard.instance);
|
||||
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
(window as any).term = term; // Expose `term` to window for debugging purposes
|
||||
term.onResize((size: { cols: number, rows: number }) => {
|
||||
if (!pid) {
|
||||
return;
|
||||
}
|
||||
const cols = size.cols;
|
||||
const rows = size.rows;
|
||||
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
|
||||
const pixelWidth = Math.round(term!.dimensions?.css?.canvas?.width ?? 0);
|
||||
const pixelHeight = Math.round(term!.dimensions?.css?.canvas?.height ?? 0);
|
||||
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows + '&pixelWidth=' + pixelWidth + '&pixelHeight=' + pixelHeight;
|
||||
|
||||
fetch(url, { method: 'POST' });
|
||||
});
|
||||
@@ -333,7 +341,7 @@ function createTerminal(): Terminal {
|
||||
if (addons.webgl.instance) {
|
||||
try {
|
||||
typedTerm.loadAddon(addons.webgl.instance);
|
||||
term.open(terminalContainer);
|
||||
term.open(terminalContainer!);
|
||||
} catch (e) {
|
||||
console.warn('error during loading webgl addon:', e);
|
||||
addons.webgl.instance.dispose();
|
||||
@@ -342,7 +350,7 @@ function createTerminal(): Terminal {
|
||||
}
|
||||
if (!typedTerm.element) {
|
||||
// webgl loading failed for some reason, attach with DOM renderer
|
||||
term.open(terminalContainer);
|
||||
term.open(terminalContainer!);
|
||||
}
|
||||
|
||||
term.focus();
|
||||
@@ -352,13 +360,13 @@ function createTerminal(): Terminal {
|
||||
if (optionsWindow.autoResize) {
|
||||
// In general this should be debounced to avoid excessive work on the main
|
||||
// thread by firing the expensive resize action repeatedly
|
||||
addons.fit.instance.fit();
|
||||
addons.fit.instance!.fit();
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(terminalContainer);
|
||||
resizeObserver.observe(terminalContainer!);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
terminalContainer.style.width = document.body.clientWidth + 'px';
|
||||
terminalContainer!.style.width = document.body.clientWidth + 'px';
|
||||
});
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
@@ -372,7 +380,9 @@ function createTerminal(): Terminal {
|
||||
if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) {
|
||||
runFakeTerminal();
|
||||
} else {
|
||||
const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' });
|
||||
const pixelWidth = Math.round(term!.dimensions?.css?.canvas?.width ?? 0);
|
||||
const pixelHeight = Math.round(term!.dimensions?.css?.canvas?.height ?? 0);
|
||||
const res = await fetch('/terminals?cols=' + term!.cols + '&rows=' + term!.rows + '&pixelWidth=' + pixelWidth + '&pixelHeight=' + pixelHeight, { method: 'POST' });
|
||||
const processId = await res.text();
|
||||
pid = processId;
|
||||
socketURL += processId;
|
||||
@@ -387,52 +397,52 @@ function createTerminal(): Terminal {
|
||||
}
|
||||
|
||||
function runRealTerminal(): void {
|
||||
addons.attach.instance = new AttachAddon(socket);
|
||||
term.loadAddon(addons.attach.instance);
|
||||
term._initialized = true;
|
||||
initAddons(term);
|
||||
addons.attach.instance = new AttachAddon(socket!);
|
||||
term!.loadAddon(addons.attach.instance);
|
||||
(term as any)._initialized = true;
|
||||
initAddons(term!);
|
||||
}
|
||||
|
||||
function runFakeTerminal(): void {
|
||||
if (term._initialized) {
|
||||
if ((term as any)._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
term._initialized = true;
|
||||
initAddons(term);
|
||||
(term as any)._initialized = true;
|
||||
initAddons(term!);
|
||||
|
||||
term.prompt = () => {
|
||||
term.write('\r\n$ ');
|
||||
(term as any).prompt = () => {
|
||||
term!.write('\r\n$ ');
|
||||
};
|
||||
|
||||
term.writeln('Welcome to xterm.js');
|
||||
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
|
||||
term.writeln('Type some keys and commands to play around.');
|
||||
term.writeln('');
|
||||
term.prompt();
|
||||
term!.writeln('Welcome to xterm.js');
|
||||
term!.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
|
||||
term!.writeln('Type some keys and commands to play around.');
|
||||
term!.writeln('');
|
||||
(term as any).prompt();
|
||||
|
||||
term.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
|
||||
term!.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
|
||||
const ev = e.domEvent;
|
||||
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
|
||||
|
||||
if (ev.keyCode === 13) {
|
||||
term.prompt();
|
||||
(term as any).prompt();
|
||||
} else if (ev.keyCode === 8) {
|
||||
// Do not delete the prompt
|
||||
if (term._core.buffer.x > 2) {
|
||||
term.write('\b \b');
|
||||
if ((term as any)._core.buffer.x > 2) {
|
||||
term!.write('\b \b');
|
||||
}
|
||||
} else if (printable) {
|
||||
term.write(e.key);
|
||||
term!.write(e.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateTerminalContainerBackground(): void {
|
||||
if (term.options.allowTransparency) {
|
||||
terminalContainer.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px';
|
||||
if (term!.options.allowTransparency) {
|
||||
terminalContainer!.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px';
|
||||
} else {
|
||||
terminalContainer.style.background = term.options.theme?.background ?? '#000000';
|
||||
terminalContainer!.style.background = term!.options.theme?.background ?? '#000000';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,19 +452,19 @@ function initAddons(term: Terminal): void {
|
||||
function postInitWebgl(): void {
|
||||
controlBar.setTabVisible('addon-webgl', true);
|
||||
setTimeout(() => {
|
||||
addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
||||
addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
|
||||
addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
||||
addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
||||
}, 500);
|
||||
}
|
||||
function preDisposeWebgl(): void {
|
||||
controlBar.setTabVisible('addon-webgl', false);
|
||||
if (addons.webgl.instance.textureAtlas) {
|
||||
addons.webgl.instance.textureAtlas.remove();
|
||||
if (addons.webgl.instance!.textureAtlas) {
|
||||
addons.webgl.instance!.textureAtlas.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(addons).forEach((name: AddonType) => {
|
||||
(Object.keys(addons) as AddonType[]).forEach(name => {
|
||||
const addon = addons[name];
|
||||
const checkbox = document.createElement('input') as HTMLInputElement;
|
||||
checkbox.type = 'checkbox';
|
||||
@@ -469,12 +479,12 @@ function initAddons(term: Terminal): void {
|
||||
term.unicode.activeVersion = '15-graphemes';
|
||||
}
|
||||
if (name === 'search' && checkbox.checked) {
|
||||
addons[name].instance.onDidChangeResults(e => updateFindResults(e));
|
||||
addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
|
||||
}
|
||||
addDomListener(checkbox, 'change', () => {
|
||||
if (name === 'image') {
|
||||
if (checkbox.checked) {
|
||||
const ctorOptionsJson = document.querySelector<HTMLTextAreaElement>('#image-options').value;
|
||||
const ctorOptionsJson = document.querySelector<HTMLTextAreaElement>('#image-options')!.value;
|
||||
addon.instance = ctorOptionsJson
|
||||
? new addons[name].ctor(JSON.parse(ctorOptionsJson))
|
||||
: new addons[name].ctor();
|
||||
@@ -500,9 +510,15 @@ function initAddons(term: Terminal): void {
|
||||
term.unicode.activeVersion = '15-graphemes';
|
||||
} else if (name === 'search') {
|
||||
controlBar.setTabVisible('addon-search', true);
|
||||
addons[name].instance.onDidChangeResults(e => updateFindResults(e));
|
||||
addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
|
||||
} else if (name === 'serialize') {
|
||||
controlBar.setTabVisible('addon-serialize', true);
|
||||
} else if (name === 'ligatures') {
|
||||
controlBar.setTabVisible('addon-ligatures', true);
|
||||
} else if (name === 'progress') {
|
||||
controlBar.setTabVisible('addon-progress', true);
|
||||
} else if (name === 'webLinks') {
|
||||
controlBar.setTabVisible('addon-web-links', true);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
@@ -519,6 +535,12 @@ function initAddons(term: Terminal): void {
|
||||
controlBar.setTabVisible('addon-search', false);
|
||||
} else if (name === 'serialize') {
|
||||
controlBar.setTabVisible('addon-serialize', false);
|
||||
} else if (name === 'ligatures') {
|
||||
controlBar.setTabVisible('addon-ligatures', false);
|
||||
} else if (name === 'progress') {
|
||||
controlBar.setTabVisible('addon-progress', false);
|
||||
} else if (name === 'webLinks') {
|
||||
controlBar.setTabVisible('addon-web-links', false);
|
||||
}
|
||||
addon.instance!.dispose();
|
||||
addon.instance = undefined;
|
||||
@@ -590,24 +612,24 @@ function updateFindResults(e: { resultIndex: number, resultCount: number } | und
|
||||
|
||||
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||
element.addEventListener(type, handler);
|
||||
term._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
||||
(term as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
||||
}
|
||||
|
||||
function updateTerminalSize(): void {
|
||||
const width = optionsWindow.autoResize ? '100%'
|
||||
: (term.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
: ((term as any).dimensions.css.canvas.width + (term as any)._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
const height = optionsWindow.autoResize ? '100%'
|
||||
: (term.dimensions.css.canvas.height).toString() + 'px';
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
addons.fit.instance.fit();
|
||||
: ((term as any).dimensions.css.canvas.height).toString() + 'px';
|
||||
terminalContainer!.style.width = width;
|
||||
terminalContainer!.style.height = height;
|
||||
addons.fit.instance!.fit();
|
||||
}
|
||||
|
||||
(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
|
||||
function getBox(width: number, height: number): any {
|
||||
return {
|
||||
string: '+',
|
||||
style: 'font-size: 1px; padding: ' + Math.floor(height/2) + 'px ' + Math.floor(width/2) + 'px; line-height: ' + height + 'px;'
|
||||
style: 'font-size: 1px; padding: ' + Math.floor(height / 2) + 'px ' + Math.floor(width / 2) + 'px; line-height: ' + height + 'px;'
|
||||
};
|
||||
}
|
||||
if (source instanceof HTMLCanvasElement) {
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
|
||||
import { BaseWindow } from './baseWindow';
|
||||
import type { IControlWindow } from '../controlBar';
|
||||
import type { IImageAddonOptions } from '@xterm/addon-image';
|
||||
|
||||
export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addon-image';
|
||||
public readonly label = 'image';
|
||||
|
||||
private _imageStorageLimitInput: HTMLInputElement;
|
||||
private _imageShowPlaceholderCheckbox: HTMLInputElement;
|
||||
private _imageOptionsTextarea: HTMLTextAreaElement;
|
||||
private _imageStorageLimitInput!: HTMLInputElement;
|
||||
private _imageShowPlaceholderCheckbox!: HTMLInputElement;
|
||||
private _imageOptionsTextarea!: HTMLTextAreaElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
// Storage limit
|
||||
@@ -46,6 +47,36 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
this._imageOptionsTextarea.rows = 12;
|
||||
optionsLabel.appendChild(this._imageOptionsTextarea);
|
||||
container.appendChild(optionsLabel);
|
||||
|
||||
container.appendChild(document.createElement('br'));
|
||||
container.appendChild(document.createElement('br'));
|
||||
|
||||
// Sixel demos
|
||||
const dlSixel = document.createElement('dl');
|
||||
const dtSixel = document.createElement('dt');
|
||||
dtSixel.textContent = 'Sixel';
|
||||
dlSixel.appendChild(dtSixel);
|
||||
this._addDdWithButton(dlSixel, 'image-demo1', 'snake');
|
||||
this._addDdWithButton(dlSixel, 'image-demo2', 'oranges');
|
||||
container.appendChild(dlSixel);
|
||||
|
||||
// IIP demos
|
||||
const dlIip = document.createElement('dl');
|
||||
const dtIip = document.createElement('dt');
|
||||
dtIip.textContent = 'IIP (iTerm)';
|
||||
dlIip.appendChild(dtIip);
|
||||
this._addDdWithButton(dlIip, 'image-demo3', 'palette');
|
||||
container.appendChild(dlIip);
|
||||
|
||||
// Kitty demos
|
||||
const dlKitty = document.createElement('dl');
|
||||
const dtKitty = document.createElement('dt');
|
||||
dtKitty.textContent = 'Kitty';
|
||||
dlKitty.appendChild(dtKitty);
|
||||
this._addDdWithButton(dlKitty, 'image-demo-kitty1', 'palette');
|
||||
container.appendChild(dlKitty);
|
||||
|
||||
this._initImageAddonExposed();
|
||||
}
|
||||
|
||||
public get imageStorageLimitInput(): HTMLInputElement {
|
||||
@@ -59,4 +90,106 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
public get imageOptionsTextarea(): HTMLTextAreaElement {
|
||||
return this._imageOptionsTextarea;
|
||||
}
|
||||
|
||||
private _addDdWithButton(dl: HTMLElement, id: string, label: string): void {
|
||||
const dd = document.createElement('dd');
|
||||
const button = document.createElement('button');
|
||||
button.id = id;
|
||||
button.textContent = label;
|
||||
dd.appendChild(button);
|
||||
dl.appendChild(dd);
|
||||
}
|
||||
|
||||
private _initImageAddonExposed(): void {
|
||||
const imageAddon = this._addons.image.instance!;
|
||||
const defaultOptions: IImageAddonOptions = (imageAddon as any)._defaultOpts;
|
||||
const limitStorageElement = document.querySelector<HTMLInputElement>('#image-storagelimit')!;
|
||||
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
|
||||
this._addDomListener(limitStorageElement, 'change', () => {
|
||||
try {
|
||||
imageAddon.storageLimit = limitStorageElement.valueAsNumber;
|
||||
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
|
||||
console.log('changed storageLimit to', imageAddon.storageLimit);
|
||||
} catch (e) {
|
||||
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
|
||||
console.log('storageLimit at', imageAddon.storageLimit);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
const showPlaceholderElement = document.querySelector<HTMLInputElement>('#image-showplaceholder')!;
|
||||
showPlaceholderElement.checked = imageAddon.showPlaceholder;
|
||||
this._addDomListener(showPlaceholderElement, 'change', () => {
|
||||
imageAddon.showPlaceholder = showPlaceholderElement.checked;
|
||||
});
|
||||
const ctorOptionsElement = document.querySelector<HTMLTextAreaElement>('#image-options')!;
|
||||
ctorOptionsElement.value = JSON.stringify(defaultOptions, null, 2);
|
||||
|
||||
const sixelDemo = (url: string) => () => fetch(url)
|
||||
.then(resp => resp.arrayBuffer())
|
||||
.then(buffer => {
|
||||
this._terminal.write('\r\n');
|
||||
this._terminal.write(new Uint8Array(buffer));
|
||||
});
|
||||
|
||||
const iipDemo = (url: string) => () => fetch(url)
|
||||
.then(resp => resp.arrayBuffer())
|
||||
.then(buffer => {
|
||||
const data = new Uint8Array(buffer);
|
||||
let sdata = '';
|
||||
for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]);
|
||||
this._terminal.write('\r\n');
|
||||
this._terminal.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`);
|
||||
});
|
||||
|
||||
const kittyDemo = (url: string) => () => fetch(url)
|
||||
.then(resp => resp.arrayBuffer())
|
||||
.then(buffer => {
|
||||
const data = new Uint8Array(buffer);
|
||||
let sdata = '';
|
||||
for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]);
|
||||
const payload = btoa(sdata);
|
||||
this._terminal.write('\r\n');
|
||||
this._terminal.write(`\x1b_Ga=T,f=100;${payload}\x1b\\`);
|
||||
});
|
||||
|
||||
document.getElementById('image-demo1')!.addEventListener('click',
|
||||
sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six'));
|
||||
document.getElementById('image-demo2')!.addEventListener('click',
|
||||
sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
|
||||
document.getElementById('image-demo3')!.addEventListener('click',
|
||||
iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
||||
document.getElementById('image-demo-kitty1')!.addEventListener('click',
|
||||
kittyDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
||||
|
||||
// demo for image retrieval API
|
||||
this._terminal.element!.addEventListener('click', (ev: MouseEvent) => {
|
||||
if (!ev.ctrlKey || !imageAddon) return;
|
||||
|
||||
// TODO...
|
||||
// if (ev.altKey) {
|
||||
// const sel = term.getSelectionPosition();
|
||||
// if (sel) {
|
||||
// addons.image.instance
|
||||
// .extractCanvasAtBufferRange(term.getSelectionPosition())
|
||||
// ?.toBlob(data => window.open(URL.createObjectURL(data), '_blank'));
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
const pos = (this._terminal as any)._core._mouseService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
|
||||
const x = pos[0] - 1;
|
||||
const y = pos[1] - 1;
|
||||
const canvas = ev.shiftKey
|
||||
// ctrl+shift+click: get single tile
|
||||
? imageAddon.extractTileAtBufferCell(x, this._terminal.buffer.active.viewportY + y)
|
||||
// ctrl+click: get original image
|
||||
: imageAddon.getImageAtBufferCell(x, this._terminal.buffer.active.viewportY + y);
|
||||
canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank'));
|
||||
});
|
||||
}
|
||||
|
||||
private _addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||
element.addEventListener(type, handler);
|
||||
(this._terminal as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { BaseWindow } from './baseWindow';
|
||||
import type { IControlWindow } from '../controlBar';
|
||||
|
||||
export class AddonLigaturesWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addon-ligatures';
|
||||
public readonly label = 'ligatures';
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const dl = document.createElement('dl');
|
||||
const dt = document.createElement('dt');
|
||||
dt.textContent = 'Ligatures Addon';
|
||||
dl.appendChild(dt);
|
||||
|
||||
const dd = document.createElement('dd');
|
||||
const button = document.createElement('button');
|
||||
button.id = 'ligatures-test';
|
||||
button.textContent = 'Common ligatures';
|
||||
button.title = 'Write common ligatures sequences';
|
||||
button.addEventListener('click', () => this._ligaturesTest());
|
||||
dd.appendChild(button);
|
||||
dl.appendChild(dd);
|
||||
|
||||
container.appendChild(dl);
|
||||
}
|
||||
|
||||
private _ligaturesTest(): void {
|
||||
this._terminal.write([
|
||||
'',
|
||||
'-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
|
||||
'=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
|
||||
'<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
|
||||
'<~~ </ </> /> ~~> == != /= ~= <> === !== !=== =/= =!=',
|
||||
'<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
|
||||
'(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| <!-- <!---',
|
||||
'==== ===== ====== ======= ======== =========',
|
||||
'---- ----- ------ ------- -------- ---------'
|
||||
].join('\r\n'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { BaseWindow } from './baseWindow';
|
||||
import type { IControlWindow } from '../controlBar';
|
||||
import type { IProgressState } from '@xterm/addon-progress';
|
||||
|
||||
export class AddonProgressWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addon-progress';
|
||||
public readonly label = 'progress';
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const dl = document.createElement('dl');
|
||||
const dt = document.createElement('dt');
|
||||
dt.textContent = 'Progress Addon';
|
||||
dl.appendChild(dt);
|
||||
|
||||
this._addDdWithButton(dl, 'progress-run', 'full set run', '');
|
||||
this._addDdWithButton(dl, 'progress-0', 'state 0: remove', '');
|
||||
this._addDdWithButton(dl, 'progress-1', 'state 1: set 20%', '');
|
||||
this._addDdWithButton(dl, 'progress-2', 'state 2: error', '');
|
||||
this._addDdWithButton(dl, 'progress-3', 'state 3: indeterminate', '');
|
||||
this._addDdWithButton(dl, 'progress-4', 'state 4: pause', '');
|
||||
|
||||
const progressDd = document.createElement('dd');
|
||||
const progressDiv = document.createElement('div');
|
||||
progressDiv.id = 'progress-progress';
|
||||
const progressPercent = document.createElement('div');
|
||||
progressPercent.id = 'progress-percent';
|
||||
const progressIndeterminate = document.createElement('div');
|
||||
progressIndeterminate.id = 'progress-indeterminate';
|
||||
progressDiv.appendChild(progressPercent);
|
||||
progressDiv.appendChild(progressIndeterminate);
|
||||
progressDd.appendChild(progressDiv);
|
||||
dl.appendChild(progressDd);
|
||||
|
||||
const stateDd = document.createElement('dd');
|
||||
const stateDiv = document.createElement('div');
|
||||
stateDiv.id = 'progress-state';
|
||||
stateDiv.textContent = 'State:';
|
||||
stateDd.appendChild(stateDiv);
|
||||
dl.appendChild(stateDd);
|
||||
|
||||
container.appendChild(dl);
|
||||
|
||||
this._initProgress();
|
||||
this._addProgressStyles(container);
|
||||
}
|
||||
|
||||
private _addDdWithButton(dl: HTMLElement, id: string, label: string, title: string): void {
|
||||
const dd = document.createElement('dd');
|
||||
const button = document.createElement('button');
|
||||
button.id = id;
|
||||
button.textContent = label;
|
||||
if (title) {
|
||||
button.title = title;
|
||||
}
|
||||
dd.appendChild(button);
|
||||
dl.appendChild(dd);
|
||||
}
|
||||
|
||||
private _initProgress(): void {
|
||||
const states = { 0: 'remove', 1: 'set', 2: 'error', 3: 'indeterminate', 4: 'pause' };
|
||||
const colors = { 0: '', 1: 'green', 2: 'red', 3: '', 4: 'yellow' };
|
||||
|
||||
function progressHandler({ state, value }: IProgressState): void {
|
||||
// Simulate windows taskbar hack by windows terminal:
|
||||
// Since the taskbar has no means to indicate error/pause state other than by coloring
|
||||
// the current progress, we move 0 to 10% and distribute higher values in the remaining 90 %
|
||||
// NOTE: This is most likely not what you want to do for other progress indicators,
|
||||
// that have a proper visual state for error/paused.
|
||||
value = Math.min(10 + value * 0.9, 100);
|
||||
document.getElementById('progress-percent')!.style.width = `${value}%`;
|
||||
document.getElementById('progress-percent')!.style.backgroundColor = colors[state];
|
||||
document.getElementById('progress-state')!.innerText = `State: ${states[state]}`;
|
||||
|
||||
document.getElementById('progress-percent')!.style.display = state === 3 ? 'none' : 'block';
|
||||
document.getElementById('progress-indeterminate')!.style.display = state === 3 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
const progressAddon = this._addons.progress.instance!;
|
||||
progressAddon.onChange(progressHandler);
|
||||
|
||||
const initialProgress = progressAddon.progress;
|
||||
progressHandler(initialProgress);
|
||||
|
||||
document.getElementById('progress-run')!.addEventListener('click', async () => {
|
||||
this._terminal.write('\x1b]9;4;0\x1b\\');
|
||||
for (let i = 0; i <= 100; i += 5) {
|
||||
this._terminal.write(`\x1b]9;4;1;${i}\x1b\\`);
|
||||
await new Promise(res => setTimeout(res, 200));
|
||||
}
|
||||
});
|
||||
document.getElementById('progress-0')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;0\x1b\\'));
|
||||
document.getElementById('progress-1')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;1;20\x1b\\'));
|
||||
document.getElementById('progress-2')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;2\x1b\\'));
|
||||
document.getElementById('progress-3')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;3\x1b\\'));
|
||||
document.getElementById('progress-4')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;4\x1b\\'));
|
||||
}
|
||||
|
||||
private _addProgressStyles(container: HTMLElement): void {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#progress-progress {
|
||||
border: 1px solid black;
|
||||
height: 10px;
|
||||
}
|
||||
#progress-percent {
|
||||
height: 100%;
|
||||
}
|
||||
#progress-indeterminate {
|
||||
display: none;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
#progress-indeterminate:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0px;
|
||||
width: 50px;
|
||||
height: 10px;
|
||||
background: blue;
|
||||
animation: ballbns 1s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes ballbns {
|
||||
0% { left: 0; transform: translateX(0%); }
|
||||
100% { left: 100%; transform: translateX(-100%); }
|
||||
}
|
||||
`;
|
||||
container.appendChild(style);
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,13 @@ export class AddonSearchWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addon-search';
|
||||
public readonly label = 'search';
|
||||
|
||||
private _findNextInput: HTMLInputElement;
|
||||
private _findPreviousInput: HTMLInputElement;
|
||||
private _findResultsSpan: HTMLElement;
|
||||
private _regexCheckbox: HTMLInputElement;
|
||||
private _caseSensitiveCheckbox: HTMLInputElement;
|
||||
private _wholeWordCheckbox: HTMLInputElement;
|
||||
private _highlightAllMatchesCheckbox: HTMLInputElement;
|
||||
private _findNextInput!: HTMLInputElement;
|
||||
private _findPreviousInput!: HTMLInputElement;
|
||||
private _findResultsSpan!: HTMLElement;
|
||||
private _regexCheckbox!: HTMLInputElement;
|
||||
private _caseSensitiveCheckbox!: HTMLInputElement;
|
||||
private _wholeWordCheckbox!: HTMLInputElement;
|
||||
private _highlightAllMatchesCheckbox!: HTMLInputElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const wrapper = document.createElement('div');
|
||||
|
||||
@@ -10,10 +10,10 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addon-serialize';
|
||||
public readonly label = 'serialize';
|
||||
|
||||
private _serializeOutputPre: HTMLPreElement;
|
||||
private _htmlSerializeOutputPre: HTMLPreElement;
|
||||
private _htmlSerializeOutputResult: HTMLElement;
|
||||
private _writeToTerminalCheckbox: HTMLInputElement;
|
||||
private _serializeOutputPre!: HTMLPreElement;
|
||||
private _htmlSerializeOutputPre!: HTMLPreElement;
|
||||
private _htmlSerializeOutputResult!: HTMLElement;
|
||||
private _writeToTerminalCheckbox!: HTMLInputElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const wrapper = document.createElement('div');
|
||||
@@ -64,7 +64,7 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow {
|
||||
}
|
||||
|
||||
private _serializeButtonHandler(): void {
|
||||
const output = this._addons.serialize.instance.serialize();
|
||||
const output = this._addons.serialize.instance!.serialize();
|
||||
const outputString = JSON.stringify(output);
|
||||
|
||||
this._serializeOutputPre.innerText = outputString;
|
||||
@@ -75,7 +75,7 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow {
|
||||
}
|
||||
|
||||
private _htmlSerializeButtonHandler(): void {
|
||||
const output = this._addons.serialize.instance.serializeAsHTML();
|
||||
const output = this._addons.serialize.instance!.serializeAsHTML();
|
||||
this._htmlSerializeOutputPre.innerText = output;
|
||||
|
||||
// Deprecated, but the most supported for now.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { BaseWindow } from './baseWindow';
|
||||
import type { IControlWindow } from '../controlBar';
|
||||
|
||||
export class AddonWebLinksWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addon-web-links';
|
||||
public readonly label = 'web-links';
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const dl = document.createElement('dl');
|
||||
const dt = document.createElement('dt');
|
||||
dt.textContent = 'Weblinks Addon';
|
||||
dl.appendChild(dt);
|
||||
|
||||
const dd = document.createElement('dd');
|
||||
const button = document.createElement('button');
|
||||
button.id = 'weblinks-test';
|
||||
button.textContent = 'Test URLs';
|
||||
button.title = 'Various url conditions from demo data, hover&click to test';
|
||||
button.addEventListener('click', () => this._testWeblinks());
|
||||
dd.appendChild(button);
|
||||
dl.appendChild(dd);
|
||||
|
||||
container.appendChild(dl);
|
||||
}
|
||||
|
||||
private _testWeblinks(): void {
|
||||
const linkExamples = `
|
||||
aaa http://example.com aaa http://example.com aaa
|
||||
\uFFE5\uFFE5\uFFE5 http://example.com aaa http://example.com aaa
|
||||
aaa http://example.com \uFFE5\uFFE5\uFFE5 http://example.com aaa
|
||||
\uFFE5\uFFE5\uFFE5 http://example.com \uFFE5\uFFE5\uFFE5 http://example.com aaa
|
||||
aaa https://ko.wikipedia.org/wiki/\uC704\uD0A4\uBC31\uACFC:\uB300\uBB38 aaa https://ko.wikipedia.org/wiki/\uC704\uD0A4\uBC31\uACFC:\uB300\uBB38 aaa
|
||||
\uFFE5\uFFE5\uFFE5 https://ko.wikipedia.org/wiki/\uC704\uD0A4\uBC31\uACFC:\uB300\uBB38 aaa https://ko.wikipedia.org/wiki/\uC704\uD0A4\uBC31\uACFC:\uB300\uBB38 \uFFE5\uFFE5\uFFE5
|
||||
aaa http://test:password@example.com/some_path aaa
|
||||
brackets enclosed:
|
||||
aaa [http://example.de] aaa
|
||||
aaa (http://example.de) aaa
|
||||
aaa <http://example.de> aaa
|
||||
aaa {http://example.de} aaa
|
||||
ipv6 https://[::1]/with/some?vars=and&a#hash aaa
|
||||
stop at final '.': This is a sentence with an url to http://example.com.
|
||||
stop at final '?': Is this the right url http://example.com/?
|
||||
stop at final '?': Maybe this one http://example.com/with?arguments=false?
|
||||
`;
|
||||
this._terminal.write(linkExamples.split('\n').join('\r\n'));
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export class AddonsWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addons';
|
||||
public readonly label = 'Addons';
|
||||
|
||||
private _addonsContainer: HTMLElement;
|
||||
private _addonsContainer!: HTMLElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
// Description
|
||||
|
||||
@@ -30,14 +30,14 @@ export class CellInspectorWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'cell-inspector';
|
||||
public readonly label = 'Cell Inspector';
|
||||
|
||||
private _container: HTMLElement;
|
||||
private _positionEl: HTMLElement;
|
||||
private _charEl: HTMLElement;
|
||||
private _codeEl: HTMLElement;
|
||||
private _widthEl: HTMLElement;
|
||||
private _fgEl: HTMLElement;
|
||||
private _bgEl: HTMLElement;
|
||||
private _attrsEl: HTMLElement;
|
||||
private _container!: HTMLElement;
|
||||
private _positionEl!: HTMLElement;
|
||||
private _charEl!: HTMLElement;
|
||||
private _codeEl!: HTMLElement;
|
||||
private _widthEl!: HTMLElement;
|
||||
private _fgEl!: HTMLElement;
|
||||
private _bgEl!: HTMLElement;
|
||||
private _attrsEl!: HTMLElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
this._container = container;
|
||||
|
||||
@@ -10,7 +10,7 @@ export class GpuWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'gpu';
|
||||
public readonly label = 'WebGL';
|
||||
|
||||
private _textureAtlasContainer: HTMLElement;
|
||||
private _textureAtlasContainer!: HTMLElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const zoomCheckbox = document.createElement('input');
|
||||
|
||||
@@ -80,8 +80,8 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'options';
|
||||
public readonly label = 'Options';
|
||||
|
||||
private _container: HTMLElement;
|
||||
private _optionsContainer: HTMLElement;
|
||||
private _container!: HTMLElement;
|
||||
private _optionsContainer!: HTMLElement;
|
||||
private _autoResize: boolean = true;
|
||||
|
||||
constructor(
|
||||
@@ -109,7 +109,6 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
|
||||
public initOptions(addDomListener: (el: HTMLElement, type: string, handler: (...args: any[]) => any) => void): void {
|
||||
const blacklistedOptions = [
|
||||
'cancelEvents',
|
||||
'convertEol',
|
||||
'termName',
|
||||
'cols', 'rows',
|
||||
@@ -143,7 +142,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
const booleanOptions: string[] = [];
|
||||
const numberOptions: string[] = [];
|
||||
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
||||
switch (typeof this._terminal.options[o]) {
|
||||
switch (typeof (this._terminal.options as Record<string, unknown>)[o]) {
|
||||
case 'boolean':
|
||||
booleanOptions.push(o);
|
||||
break;
|
||||
@@ -160,25 +159,25 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
let html = '';
|
||||
html += '<div class="option-group">';
|
||||
booleanOptions.forEach(o => {
|
||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${this._terminal.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
|
||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${(this._terminal.options as Record<string, unknown>)[o] ? 'checked' : ''}/> ${o}</label></div>`;
|
||||
});
|
||||
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
|
||||
const checked = this._terminal.options[parent]?.[prop] ?? false;
|
||||
const checked = (this._terminal.options as Record<string, Record<string, unknown> | undefined>)[parent]?.[prop] ?? false;
|
||||
html += `<div class="option"><label><input id="opt-${label.replace('.', '-')}" type="checkbox" ${checked ? 'checked' : ''}/> ${label}</label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
numberOptions.forEach(o => {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${this._terminal.options[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${(this._terminal.options as Record<string, unknown>)[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
Object.keys(stringOptions).forEach(o => {
|
||||
if (o === 'colsRows') {
|
||||
html += `<div class="option"><label>size (<var>cols</var><code>x</code><var>rows</var> or <code>auto</code>) <input id="opt-${o}" type="text" value="auto"/></label></div>`;
|
||||
} else if (stringOptions[o]) {
|
||||
const selectedOption = o === 'theme' ? 'xtermjs' : this._terminal.options[o];
|
||||
const selectedOption = o === 'theme' ? 'xtermjs' : (this._terminal.options as Record<string, unknown>)[o];
|
||||
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o]!.map(v => `<option ${v === selectedOption ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
||||
} else {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${this._terminal.options[o]}"/></label></div>`;
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${(this._terminal.options as Record<string, unknown>)[o]}"/></label></div>`;
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
@@ -190,7 +189,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.checked);
|
||||
this._terminal.options[o] = input.checked;
|
||||
(this._terminal.options as Record<string, unknown>)[o] = input.checked;
|
||||
if (o ==='allowTransparency') {
|
||||
this._terminal.options.theme = this._getTheme();
|
||||
this._handlers.updateTerminalContainerBackground();
|
||||
@@ -201,7 +200,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
const input = document.getElementById(`opt-${label.replace('.', '-')}`) as HTMLInputElement;
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', label, input.checked);
|
||||
this._terminal.options[parent] = { ...this._terminal.options[parent], [prop]: input.checked };
|
||||
(this._terminal.options as Record<string, unknown>)[parent] = { ...(this._terminal.options as Record<string, Record<string, unknown> | undefined>)[parent], [prop]: input.checked };
|
||||
});
|
||||
});
|
||||
numberOptions.forEach(o => {
|
||||
@@ -216,7 +215,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
this._terminal.options.scrollback = parseInt(input.value);
|
||||
setTimeout(() => this._handlers.updateTerminalSize(), 5);
|
||||
} else {
|
||||
this._terminal.options[o] = parseInt(input.value);
|
||||
(this._terminal.options as Record<string, unknown>)[o] = parseInt(input.value);
|
||||
}
|
||||
this._handlers.updateTerminalSize();
|
||||
});
|
||||
@@ -225,7 +224,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.value);
|
||||
let value: any = input.value;
|
||||
let value: unknown = input.value;
|
||||
if (o === 'colsRows') {
|
||||
const m = input.value.match(/^([0-9]+)x([0-9]+)$/);
|
||||
if (m) {
|
||||
@@ -239,7 +238,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
} else if (o === 'theme') {
|
||||
value = this._getTheme();
|
||||
}
|
||||
this._terminal.options[o] = value;
|
||||
(this._terminal.options as Record<string, unknown>)[o] = value;
|
||||
if (o === 'theme') {
|
||||
this._handlers.updateTerminalContainerBackground();
|
||||
}
|
||||
@@ -259,11 +258,11 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
}
|
||||
|
||||
private _getTheme(): ITheme {
|
||||
const input = document.querySelector<HTMLInputElement>('#opt-theme');
|
||||
let theme: ITheme;
|
||||
const input = document.querySelector<HTMLInputElement>('#opt-theme')!;
|
||||
let theme: ITheme = {};
|
||||
switch (input.value) {
|
||||
case 'default':
|
||||
theme = undefined;
|
||||
theme = {};
|
||||
break;
|
||||
case 'xtermjs':
|
||||
theme = { ...xtermjsTheme };
|
||||
|
||||
@@ -10,7 +10,7 @@ export class StyleWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'style';
|
||||
public readonly label = 'Style';
|
||||
|
||||
private _paddingElement: HTMLInputElement;
|
||||
private _paddingElement!: HTMLInputElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const wrapper = document.createElement('div');
|
||||
|
||||
@@ -8,8 +8,6 @@ import type { IControlWindow } from '../controlBar';
|
||||
import { BaseWindow } from './baseWindow';
|
||||
import type { IDisposable, Terminal } from '@xterm/xterm';
|
||||
import type { AddonCollection } from 'types';
|
||||
import type { IProgressState } from '@xterm/addon-progress';
|
||||
import type { IImageAddonOptions } from '@xterm/addon-image';
|
||||
|
||||
export class TestWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'test';
|
||||
@@ -66,61 +64,13 @@ export class TestWindow extends BaseWindow implements IControlWindow {
|
||||
this._addDdWithButton(dl, 'add-overview-ruler', 'Add Overview Ruler', 'Add an overview ruler to the terminal', () => addOverviewRuler(this._terminal));
|
||||
this._addDdWithButton(dl, 'decoration-stress-test', 'Stress Test', 'Toggle between adding and removing a decoration to each line', () => decorationStressTest(this._terminal));
|
||||
|
||||
// Ligatures Addon section
|
||||
this._addDt(dl, 'Ligatures Addon');
|
||||
this._addDdWithButton(dl, 'ligatures-test', 'Common ligatures', 'Write common ligatures sequences', () => ligaturesTest(this._terminal));
|
||||
|
||||
// Weblinks Addon section
|
||||
this._addDt(dl, 'Weblinks Addon');
|
||||
this._addDdWithButton(dl, 'weblinks-test', 'Test URLs', 'Various url conditions from demo data, hover&click to test', () => testWeblinks(this._terminal));
|
||||
|
||||
// Image Test section
|
||||
this._addDt(dl, 'Image Test');
|
||||
this._addDdWithButton(dl, 'image-demo1', 'snake (sixel)', '');
|
||||
this._addDdWithButton(dl, 'image-demo2', 'oranges (sixel)', '');
|
||||
this._addDdWithButton(dl, 'image-demo3', 'palette (iip)', '');
|
||||
|
||||
// Events Test section
|
||||
this._addDt(dl, 'Events Test');
|
||||
this._addDdWithButton(dl, 'event-focus', 'focus', '', () => this._terminal.focus());
|
||||
this._addDdWithButton(dl, 'event-blur', 'blur', '', () => this._terminal.blur());
|
||||
|
||||
// Progress Addon section
|
||||
this._addDt(dl, 'Progress Addon');
|
||||
this._addDdWithButton(dl, 'progress-run', 'full set run', '');
|
||||
this._addDdWithButton(dl, 'progress-0', 'state 0: remove', '');
|
||||
this._addDdWithButton(dl, 'progress-1', 'state 1: set 20%', '');
|
||||
this._addDdWithButton(dl, 'progress-2', 'state 2: error', '');
|
||||
this._addDdWithButton(dl, 'progress-3', 'state 3: indeterminate', '');
|
||||
this._addDdWithButton(dl, 'progress-4', 'state 4: pause', '');
|
||||
|
||||
// Progress bar
|
||||
const progressDd = document.createElement('dd');
|
||||
const progressDiv = document.createElement('div');
|
||||
progressDiv.id = 'progress-progress';
|
||||
const progressPercent = document.createElement('div');
|
||||
progressPercent.id = 'progress-percent';
|
||||
const progressIndeterminate = document.createElement('div');
|
||||
progressIndeterminate.id = 'progress-indeterminate';
|
||||
progressDiv.appendChild(progressPercent);
|
||||
progressDiv.appendChild(progressIndeterminate);
|
||||
progressDd.appendChild(progressDiv);
|
||||
dl.appendChild(progressDd);
|
||||
|
||||
// Progress state
|
||||
const stateDd = document.createElement('dd');
|
||||
const stateDiv = document.createElement('div');
|
||||
stateDiv.id = 'progress-state';
|
||||
stateDiv.textContent = 'State:';
|
||||
stateDd.appendChild(stateDiv);
|
||||
dl.appendChild(stateDd);
|
||||
|
||||
wrapper.appendChild(dl);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
initProgress(this._terminal, this._addons);
|
||||
this._addProgressStyles(container);
|
||||
initImageAddonExposed(this._terminal, this._addons);
|
||||
}
|
||||
|
||||
private _addDt(dl: HTMLElement, text: string): void {
|
||||
@@ -139,7 +89,9 @@ export class TestWindow extends BaseWindow implements IControlWindow {
|
||||
}
|
||||
dd.appendChild(button);
|
||||
dl.appendChild(dd);
|
||||
button.addEventListener('click', handler);
|
||||
if (handler) {
|
||||
button.addEventListener('click', handler);
|
||||
}
|
||||
}
|
||||
|
||||
private _addDdWithCheckbox(dl: HTMLElement, id: string, label: string, title: string, checked: boolean): void {
|
||||
@@ -159,38 +111,6 @@ export class TestWindow extends BaseWindow implements IControlWindow {
|
||||
dl.appendChild(dd);
|
||||
}
|
||||
|
||||
private _addProgressStyles(container: HTMLElement): void {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#progress-progress {
|
||||
border: 1px solid black;
|
||||
height: 10px;
|
||||
}
|
||||
#progress-percent {
|
||||
height: 100%;
|
||||
}
|
||||
#progress-indeterminate {
|
||||
display: none;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
#progress-indeterminate:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0px;
|
||||
width: 50px;
|
||||
height: 10px;
|
||||
background: blue;
|
||||
animation: ballbns 1s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes ballbns {
|
||||
0% { left: 0; transform: translateX(0%); }
|
||||
100% { left: 100%; transform: translateX(-100%); }
|
||||
}
|
||||
`;
|
||||
container.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,7 +385,7 @@ function customGlyphAlignmentHandler(term: Terminal): void {
|
||||
while (fillChars.length > 0) {
|
||||
const batch = fillChars.splice(0, 10);
|
||||
for (const fillChar of batch) {
|
||||
term.write(`${fillChar.codePointAt(0).toString(16).toUpperCase().padEnd(5, ' ')} `);
|
||||
term.write(`${fillChar.codePointAt(0)!.toString(16).toUpperCase().padEnd(5, ' ')} `);
|
||||
}
|
||||
term.write('\n\r');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
@@ -751,42 +671,6 @@ Test BG-colored Erase (BCE):
|
||||
term.write(data.split('\n').join('\r\n'));
|
||||
}
|
||||
|
||||
function ligaturesTest(term: Terminal): void {
|
||||
term.write([
|
||||
'',
|
||||
'-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
|
||||
'=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
|
||||
'<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
|
||||
'<~~ </ </> /> ~~> == != /= ~= <> === !== !=== =/= =!=',
|
||||
'<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
|
||||
'(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| <!-- <!---',
|
||||
'==== ===== ====== ======= ======== =========',
|
||||
'---- ----- ------ ------- -------- ---------'
|
||||
].join('\r\n'));
|
||||
}
|
||||
|
||||
function testWeblinks(term: Terminal): void {
|
||||
const linkExamples = `
|
||||
aaa http://example.com aaa http://example.com aaa
|
||||
¥¥¥ http://example.com aaa http://example.com aaa
|
||||
aaa http://example.com ¥¥¥ http://example.com aaa
|
||||
¥¥¥ http://example.com ¥¥¥ http://example.com aaa
|
||||
aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa
|
||||
¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥
|
||||
aaa http://test:password@example.com/some_path aaa
|
||||
brackets enclosed:
|
||||
aaa [http://example.de] aaa
|
||||
aaa (http://example.de) aaa
|
||||
aaa <http://example.de> aaa
|
||||
aaa {http://example.de} aaa
|
||||
ipv6 https://[::1]/with/some?vars=and&a#hash aaa
|
||||
stop at final '.': This is a sentence with an url to http://example.com.
|
||||
stop at final '?': Is this the right url http://example.com/?
|
||||
stop at final '?': Maybe this one http://example.com/with?arguments=false?
|
||||
`;
|
||||
term.write(linkExamples.split('\n').join('\r\n'));
|
||||
}
|
||||
|
||||
function loadTest(term: Terminal, addons: AddonCollection): void {
|
||||
const rendererName = addons.webgl.instance ? 'webgl' : 'dom';
|
||||
const testData = [];
|
||||
@@ -864,7 +748,7 @@ function addDecoration(term: Terminal, dim: number = 1): void {
|
||||
foregroundColor: '#00FE00',
|
||||
overviewRulerOptions: { color: '#ef292980', position: 'left' }
|
||||
});
|
||||
decoration.onRender((e: HTMLElement) => {
|
||||
decoration?.onRender((e: HTMLElement) => {
|
||||
e.style.right = '100%';
|
||||
e.style.backgroundColor = '#ef292980';
|
||||
});
|
||||
@@ -896,133 +780,17 @@ function decorationStressTest(term: Terminal): void {
|
||||
for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) {
|
||||
for (let y = 0; y < term.buffer.active.length; y++) {
|
||||
const cursorOffsetY = y - cursorY;
|
||||
decorationStressTestDecorations.push(term.registerDecoration({
|
||||
const decoration = term.registerDecoration({
|
||||
marker: term.registerMarker(cursorOffsetY),
|
||||
x,
|
||||
width: 4,
|
||||
backgroundColor: '#FF0000',
|
||||
overviewRulerOptions: { color: '#FF0000' }
|
||||
}));
|
||||
});
|
||||
if (decoration) {
|
||||
decorationStressTestDecorations.push(decoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initProgress(term: Terminal, addons: AddonCollection): void {
|
||||
const STATES = { 0: 'remove', 1: 'set', 2: 'error', 3: 'indeterminate', 4: 'pause' };
|
||||
const COLORS = { 0: '', 1: 'green', 2: 'red', 3: '', 4: 'yellow' };
|
||||
|
||||
function progressHandler({ state, value }: IProgressState): void {
|
||||
// Simulate windows taskbar hack by windows terminal:
|
||||
// Since the taskbar has no means to indicate error/pause state other than by coloring
|
||||
// the current progress, we move 0 to 10% and distribute higher values in the remaining 90 %
|
||||
// NOTE: This is most likely not what you want to do for other progress indicators,
|
||||
// that have a proper visual state for error/paused.
|
||||
value = Math.min(10 + value * 0.9, 100);
|
||||
document.getElementById('progress-percent').style.width = `${value}%`;
|
||||
document.getElementById('progress-percent').style.backgroundColor = COLORS[state];
|
||||
document.getElementById('progress-state').innerText = `State: ${STATES[state]}`;
|
||||
|
||||
document.getElementById('progress-percent').style.display = state === 3 ? 'none' : 'block';
|
||||
document.getElementById('progress-indeterminate').style.display = state === 3 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
const progressAddon = addons.progress.instance;
|
||||
progressAddon.onChange(progressHandler);
|
||||
|
||||
// apply initial state once to make it visible on page load
|
||||
const initialProgress = progressAddon.progress;
|
||||
progressHandler(initialProgress);
|
||||
|
||||
document.getElementById('progress-run').addEventListener('click', async () => {
|
||||
term.write('\x1b]9;4;0\x1b\\');
|
||||
for (let i = 0; i <= 100; i += 5) {
|
||||
term.write(`\x1b]9;4;1;${i}\x1b\\`);
|
||||
await new Promise(res => setTimeout(res, 200));
|
||||
}
|
||||
});
|
||||
document.getElementById('progress-0').addEventListener('click', () => term.write('\x1b]9;4;0\x1b\\'));
|
||||
document.getElementById('progress-1').addEventListener('click', () => term.write('\x1b]9;4;1;20\x1b\\'));
|
||||
document.getElementById('progress-2').addEventListener('click', () => term.write('\x1b]9;4;2\x1b\\'));
|
||||
document.getElementById('progress-3').addEventListener('click', () => term.write('\x1b]9;4;3\x1b\\'));
|
||||
document.getElementById('progress-4').addEventListener('click', () => term.write('\x1b]9;4;4\x1b\\'));
|
||||
}
|
||||
|
||||
function initImageAddonExposed(term: Terminal, addons: AddonCollection): void {
|
||||
const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts;
|
||||
const limitStorageElement = document.querySelector<HTMLInputElement>('#image-storagelimit');
|
||||
limitStorageElement.valueAsNumber = addons.image.instance.storageLimit;
|
||||
addDomListener(term, limitStorageElement, 'change', () => {
|
||||
try {
|
||||
addons.image.instance.storageLimit = limitStorageElement.valueAsNumber;
|
||||
limitStorageElement.valueAsNumber = addons.image.instance.storageLimit;
|
||||
console.log('changed storageLimit to', addons.image.instance.storageLimit);
|
||||
} catch (e) {
|
||||
limitStorageElement.valueAsNumber = addons.image.instance.storageLimit;
|
||||
console.log('storageLimit at', addons.image.instance.storageLimit);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
const showPlaceholderElement = document.querySelector<HTMLInputElement>('#image-showplaceholder');
|
||||
showPlaceholderElement.checked = addons.image.instance.showPlaceholder;
|
||||
addDomListener(term, showPlaceholderElement, 'change', () => {
|
||||
addons.image.instance.showPlaceholder = showPlaceholderElement.checked;
|
||||
});
|
||||
const ctorOptionsElement = document.querySelector<HTMLTextAreaElement>('#image-options');
|
||||
ctorOptionsElement.value = JSON.stringify(DEFAULT_OPTIONS, null, 2);
|
||||
|
||||
const sixelDemo = (url: string) => () => fetch(url)
|
||||
.then(resp => resp.arrayBuffer())
|
||||
.then(buffer => {
|
||||
term.write('\r\n');
|
||||
term.write(new Uint8Array(buffer));
|
||||
});
|
||||
|
||||
const iipDemo = (url: string) => () => fetch(url)
|
||||
.then(resp => resp.arrayBuffer())
|
||||
.then(buffer => {
|
||||
const data = new Uint8Array(buffer);
|
||||
let sdata = '';
|
||||
for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]);
|
||||
term.write('\r\n');
|
||||
term.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`);
|
||||
});
|
||||
|
||||
document.getElementById('image-demo1').addEventListener('click',
|
||||
sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six'));
|
||||
document.getElementById('image-demo2').addEventListener('click',
|
||||
sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
|
||||
document.getElementById('image-demo3').addEventListener('click',
|
||||
iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
||||
|
||||
// demo for image retrieval API
|
||||
term.element.addEventListener('click', (ev: MouseEvent) => {
|
||||
if (!ev.ctrlKey || !addons.image.instance) return;
|
||||
|
||||
// TODO...
|
||||
// if (ev.altKey) {
|
||||
// const sel = term.getSelectionPosition();
|
||||
// if (sel) {
|
||||
// addons.image.instance
|
||||
// .extractCanvasAtBufferRange(term.getSelectionPosition())
|
||||
// ?.toBlob(data => window.open(URL.createObjectURL(data), '_blank'));
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
const pos = (term as any)._core._mouseService!.getCoords(ev, (term as any)._core.screenElement!, term.cols, term.rows);
|
||||
const x = pos[0] - 1;
|
||||
const y = pos[1] - 1;
|
||||
const canvas = ev.shiftKey
|
||||
// ctrl+shift+click: get single tile
|
||||
? addons.image.instance.extractTileAtBufferCell(x, term.buffer.active.viewportY + y)
|
||||
// ctrl+click: get original image
|
||||
: addons.image.instance.getImageAtBufferCell(x, term.buffer.active.viewportY + y);
|
||||
canvas?.toBlob(data => window.open(URL.createObjectURL(data), '_blank'));
|
||||
});
|
||||
}
|
||||
|
||||
function addDomListener(term: Terminal, element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||
element.addEventListener(type, handler);
|
||||
(term as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
|
||||
import { BaseWindow } from './baseWindow';
|
||||
import type { IControlWindow } from '../controlBar';
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
|
||||
export class VtWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'vt';
|
||||
public readonly label = 'VT';
|
||||
|
||||
private _container: HTMLElement;
|
||||
private _term: Terminal | undefined;
|
||||
private _container!: HTMLElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
this._container = container;
|
||||
@@ -65,7 +63,7 @@ export class VtWindow extends BaseWindow implements IControlWindow {
|
||||
const writeCsiSplit = writeCsi.split('|');
|
||||
const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : '';
|
||||
const suffix = writeCsiSplit[writeCsiSplit.length - 1];
|
||||
element.addEventListener('click', () => this._term?.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
|
||||
element.addEventListener('click', () => this._terminal.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
|
||||
|
||||
const desc = document.createElement('span');
|
||||
desc.textContent = description;
|
||||
|
||||
@@ -10,7 +10,7 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addon-webgl';
|
||||
public readonly label = 'webgl';
|
||||
|
||||
private _textureAtlasContainer: HTMLElement;
|
||||
private _textureAtlasContainer!: HTMLElement;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const zoomCheckbox = document.createElement('input');
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@xterm/addon-attach": ["../../addons/addon-attach"],
|
||||
"@xterm/addon-clipboard": ["../../addons/addon-clipboard"],
|
||||
@@ -18,12 +19,11 @@
|
||||
"@xterm/addon-web-links": ["../../addons/addon-web-links"],
|
||||
"@xterm/addon-webgl": ["../../addons/addon-webgl"],
|
||||
"@xterm/addon-unicode11": ["../../addons/addon-unicode11"],
|
||||
"@xterm/addon-unicode-graphemes": ["../../addons/addon-unicode-graphemes"],
|
||||
"@xterm/addon-unicode-graphemes": [
|
||||
"../../addons/addon-unicode-graphemes"
|
||||
],
|
||||
"@xterm/addon-ligatures": ["../../addons/addon-ligatures"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../typings/xterm.d.ts"
|
||||
]
|
||||
"include": ["./**/*", "../../typings/xterm.d.ts"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { WebFontsAddon } from '@xterm/addon-web-fonts';
|
||||
import type { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import type { WebglAddon } from '@xterm/addon-webgl';
|
||||
|
||||
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures';
|
||||
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'ligatures' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl';
|
||||
|
||||
export interface IDemoAddon<T extends AddonType> {
|
||||
name: T;
|
||||
|
||||
@@ -33,6 +33,7 @@ body {
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
.banner-tabs button:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
|
||||
+18
-3
@@ -65,6 +65,8 @@ function startServer(): void {
|
||||
}
|
||||
const cols = parseInt(req.query.cols);
|
||||
const rows = parseInt(req.query.rows);
|
||||
const pixelWidth = typeof req.query.pixelWidth === 'string' ? parseInt(req.query.pixelWidth) : 0;
|
||||
const pixelHeight = typeof req.query.pixelHeight === 'string' ? parseInt(req.query.pixelHeight) : 0;
|
||||
const isWindows = process.platform === 'win32';
|
||||
const term = pty.spawn(isWindows ? 'powershell.exe' : 'bash', [], {
|
||||
name: 'xterm-256color',
|
||||
@@ -77,7 +79,13 @@ function startServer(): void {
|
||||
useConptyDll: isWindows,
|
||||
});
|
||||
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
// Set pixel dimensions immediately after spawn (pty.spawn doesn't support them)
|
||||
if (pixelWidth > 0 && pixelHeight > 0) {
|
||||
term.resize(cols, rows, { width: pixelWidth, height: pixelHeight });
|
||||
console.log('Created terminal with PID: ' + term.pid + ' (' + cols + 'x' + rows + ', ' + pixelWidth + 'px x ' + pixelHeight + 'px)');
|
||||
} else {
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
}
|
||||
terminals[term.pid] = term;
|
||||
unsentOutput[term.pid] = '';
|
||||
temporaryDisposable[term.pid] = term.onData(function(data) {
|
||||
@@ -95,10 +103,17 @@ function startServer(): void {
|
||||
const pid = parseInt(req.params.pid);
|
||||
const cols = parseInt(req.query.cols);
|
||||
const rows = parseInt(req.query.rows);
|
||||
const pixelWidth = typeof req.query.pixelWidth === 'string' ? parseInt(req.query.pixelWidth) : 0;
|
||||
const pixelHeight = typeof req.query.pixelHeight === 'string' ? parseInt(req.query.pixelHeight) : 0;
|
||||
const term = terminals[pid];
|
||||
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
if (pixelWidth > 0 && pixelHeight > 0) {
|
||||
term.resize(cols, rows, { width: pixelWidth, height: pixelHeight });
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols, ' + rows + ' rows, ' + pixelWidth + 'px x ' + pixelHeight + 'px');
|
||||
} else {
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export default tseslint.config(
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'@stylistic/comma-spacing': ['warn', { before: false, after: true }],
|
||||
'@stylistic/indent': ['warn', 2],
|
||||
'@stylistic/semi': ['warn', 'always'],
|
||||
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }],
|
||||
@@ -66,6 +67,7 @@ export default tseslint.config(
|
||||
],
|
||||
'@typescript-eslint/no-confusing-void-expression': ['warn', { ignoreArrowShorthand: true }],
|
||||
'@typescript-eslint/no-useless-constructor': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': ['warn', { ignorePrimitives: true }],
|
||||
'@typescript-eslint/prefer-namespace-keyword': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { vars: 'all', args: 'none' }],
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
@@ -140,5 +142,12 @@ export default tseslint.config(
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-member-accessibility': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Disable prefer-nullish-coalescing for files without strictNullChecks
|
||||
files: ['demo/**/*.ts', '**/*.benchmark.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@xterm/headless",
|
||||
"description": "A headless terminal component that runs in Node.js",
|
||||
"version": "5.5.0",
|
||||
"version": "6.0.0",
|
||||
"main": "lib-headless/xterm-headless.js",
|
||||
"module": "lib-headless/xterm-headless.mjs",
|
||||
"types": "typings/xterm-headless.d.ts",
|
||||
|
||||
Generated
+22
-283
@@ -41,7 +41,7 @@
|
||||
"jsdom": "^27.3.0",
|
||||
"mocha": "^10.1.0",
|
||||
"mustache": "^4.2.0",
|
||||
"node-pty": "1.1.0-beta19",
|
||||
"node-pty": "^1.2.0-beta.9",
|
||||
"nyc": "^17.1.0",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
@@ -79,9 +79,15 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.1.0"
|
||||
"xterm-wasm-parts": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"addons/addon-kitty-graphics": {
|
||||
"name": "@xterm/addon-kitty-graphics",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"addons/addon-ligatures": {
|
||||
"name": "@xterm/addon-ligatures",
|
||||
"version": "0.10.0",
|
||||
@@ -1278,102 +1284,6 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -1546,16 +1456,6 @@
|
||||
"pako": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
|
||||
@@ -3511,12 +3411,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -4868,54 +4762,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/inwasm": {
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/inwasm/-/inwasm-0.0.13.tgz",
|
||||
"integrity": "sha512-gmULhw1wfF3tQ19y0TvcNH6A5jN7IuTD51kbZuy+ittUU59d+ZTQMb53wbGQuciMrledgagL3/ohnjUj5qJikQ==",
|
||||
"node_modules/inwasm-runtime": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/inwasm-runtime/-/inwasm-runtime-0.1.2.tgz",
|
||||
"integrity": "sha512-in+Lk4d7PGwQnG1zxRe6qHB2nvvc5LUOsI9ByoJRuaDONTY8xDlaF+CvapGoPiqreJ0cMHxRzKtS0SJBccpYfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.2",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"colorette": "^2.0.20",
|
||||
"glob": "^10.0.0",
|
||||
"wabt": "^1.0.32"
|
||||
},
|
||||
"bin": {
|
||||
"inwasm": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/inwasm/node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inwasm/node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
@@ -5415,21 +5267,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
@@ -5878,15 +5715,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
@@ -6016,11 +5844,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.1.0-beta19",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta19.tgz",
|
||||
"integrity": "sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A==",
|
||||
"version": "1.2.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.9.tgz",
|
||||
"integrity": "sha512-XR3x/4OQZ7DlceespOQ99kk9jZHwmO78RuEIZtGsSPdsDi11s79VcK04aqcaK3x09Y6ORJM/KBz/oQ/p+WGRLg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0"
|
||||
}
|
||||
@@ -6423,12 +6252,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
@@ -6516,28 +6339,6 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
@@ -7531,21 +7332,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
@@ -7558,19 +7344,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
|
||||
@@ -8089,23 +7862,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/wabt": {
|
||||
"version": "1.0.39",
|
||||
"resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.39.tgz",
|
||||
"integrity": "sha512-ba+dRL/75VQQY7RkU/CgriGbkoWAfS8TDyUlJfJhJ8KhtXgMl5dhNvoPNUcQ9IWRhW8u41glMSuZeTvsYq2rRg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"wasm-decompile": "bin/wasm-decompile",
|
||||
"wasm-interp": "bin/wasm-interp",
|
||||
"wasm-objdump": "bin/wasm-objdump",
|
||||
"wasm-stats": "bin/wasm-stats",
|
||||
"wasm-strip": "bin/wasm-strip",
|
||||
"wasm-validate": "bin/wasm-validate",
|
||||
"wasm2c": "bin/wasm2c",
|
||||
"wasm2wat": "bin/wasm2wat",
|
||||
"wat2wasm": "bin/wat2wasm"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz",
|
||||
@@ -8449,24 +8205,6 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -8579,12 +8317,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xterm-wasm-parts": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.1.0.tgz",
|
||||
"integrity": "sha512-GFE8yNJfdkytGpcsOZhkL3B8XyUqkR/Du3SdxRyFbJg+BBCKm3raaaflgIM4TwNQ2AOLz01BEEuB5FZXx8aNTQ==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.3.0.tgz",
|
||||
"integrity": "sha512-V/lhvDv2Scov2ukhTNmmur6jMq2DSL8QOdQdXWgfqcAYbUf7bgixwl2sB1gYPGE21NTw2OvbAw+vANRncWzR4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inwasm": "^0.0.13"
|
||||
"inwasm-runtime": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
|
||||
+5
-4
@@ -47,16 +47,17 @@
|
||||
"esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch",
|
||||
"test": "npm run test-unit",
|
||||
"posttest": "npm run lint",
|
||||
"lint": "eslint --max-warnings 0 src/ addons/ demo/",
|
||||
"lint": "eslint --max-warnings 0 src/ addons/ demo/ test/",
|
||||
"lint-changes": "node ./bin/lint_changes.js",
|
||||
"lint-changes-fix": "node ./bin/lint_changes.js --fix",
|
||||
"lint-fix": "eslint --fix src/ addons/ demo/",
|
||||
"lint-fix": "eslint --fix src/ addons/ demo/ test/",
|
||||
"lint-api": "eslint --config eslint.config.typings.mjs --max-warnings 0 typings/",
|
||||
"test-unit": "node ./bin/test_unit.js",
|
||||
"test-unit-slow-tests": "npm run test-unit | grep \"ms)\"",
|
||||
"test-unit-coverage": "node ./bin/test_unit.js --coverage",
|
||||
"test-unit-dev": "cross-env NODE_PATH='./out' mocha",
|
||||
"test-integration": "node ./bin/test_integration.js --workers=75%",
|
||||
"test-integration-chromium": "node ./bin/test_integration.js --workers=75% \"--project=ChromeStable\"",
|
||||
"test-integration-chromium": "node ./bin/test_integration.js --workers=75% \"--project=Chromium\"",
|
||||
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
|
||||
"test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"",
|
||||
"test-integration-debug": "node ./bin/test_integration.js --workers=1 --headed --timeout=30000",
|
||||
@@ -103,7 +104,7 @@
|
||||
"jsdom": "^27.3.0",
|
||||
"mocha": "^10.1.0",
|
||||
"mustache": "^4.2.0",
|
||||
"node-pty": "1.1.0-beta19",
|
||||
"node-pty": "^1.2.0-beta.9",
|
||||
"nyc": "^17.1.0",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as Strings from 'browser/LocalizableStrings';
|
||||
import { ITerminal, IRenderDebouncer } from 'browser/Types';
|
||||
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IInstantiationService } from 'common/services/Services';
|
||||
|
||||
@@ -42,22 +42,22 @@ import { SelectionService } from 'browser/services/SelectionService';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
import { KeyboardService } from 'browser/services/KeyboardService';
|
||||
import { channels, color } from 'common/Color';
|
||||
import { channels, color, rgb } from 'common/Color';
|
||||
import { CoreTerminal } from 'common/CoreTerminal';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { C0, C1_ESCAPED } from 'common/data/EscapeSequences';
|
||||
import { C0, C1ESCAPED } from 'common/data/EscapeSequences';
|
||||
import { toRgbString } from 'common/input/XParseColor';
|
||||
import { DecorationService } from 'common/services/DecorationService';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
import { WindowsOptionsReportType } from '../common/InputHandler';
|
||||
import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Emitter, EventUtils, type IEvent } from 'common/Event';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
public textarea: HTMLTextAreaElement | undefined;
|
||||
@@ -135,15 +135,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
public readonly onBell = this._onBell.event;
|
||||
|
||||
private _onFocus = this._register(new Emitter<void>());
|
||||
public get onFocus(): Event<void> { return this._onFocus.event; }
|
||||
public get onFocus(): IEvent<void> { return this._onFocus.event; }
|
||||
private _onBlur = this._register(new Emitter<void>());
|
||||
public get onBlur(): Event<void> { return this._onBlur.event; }
|
||||
public get onBlur(): IEvent<void> { return this._onBlur.event; }
|
||||
private _onA11yCharEmitter = this._register(new Emitter<string>());
|
||||
public get onA11yChar(): Event<string> { return this._onA11yCharEmitter.event; }
|
||||
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
|
||||
private _onA11yTabEmitter = this._register(new Emitter<number>());
|
||||
public get onA11yTab(): Event<number> { return this._onA11yTabEmitter.event; }
|
||||
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }
|
||||
private _onWillOpen = this._register(new Emitter<HTMLElement>());
|
||||
public get onWillOpen(): Event<HTMLElement> { return this._onWillOpen.event; }
|
||||
public get onWillOpen(): IEvent<HTMLElement> { return this._onWillOpen.event; }
|
||||
private readonly _onDimensionsChange = this._register(new Emitter<IRenderDimensionsApi>());
|
||||
public readonly onDimensionsChange = this._onDimensionsChange.event;
|
||||
|
||||
@@ -187,10 +187,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._register(this._inputHandler.onRequestReset(() => this.reset()));
|
||||
this._register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
|
||||
this._register(this._inputHandler.onColor((event) => this._handleColorEvent(event)));
|
||||
this._register(Event.forward(this._inputHandler.onCursorMove, this._onCursorMove));
|
||||
this._register(Event.forward(this._inputHandler.onTitleChange, this._onTitleChange));
|
||||
this._register(Event.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
|
||||
this._register(Event.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));
|
||||
this._register(EventUtils.forward(this._inputHandler.onCursorMove, this._onCursorMove));
|
||||
this._register(EventUtils.forward(this._inputHandler.onTitleChange, this._onTitleChange));
|
||||
this._register(EventUtils.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
|
||||
this._register(EventUtils.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));
|
||||
|
||||
// Setup listeners
|
||||
this._register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
|
||||
@@ -235,7 +235,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
const colorRgb = color.toColorRGB(acc === 'ansi'
|
||||
? this._themeService.colors.ansi[req.index]
|
||||
: this._themeService.colors[acc]);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1ESCAPED.ST}`);
|
||||
break;
|
||||
case ColorRequestType.SET:
|
||||
if (acc === 'ansi') {
|
||||
@@ -252,6 +252,20 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the current color scheme (dark or light) based on the relative luminance
|
||||
* of the background and foreground theme colors.
|
||||
* Sends CSI ? 997 ; 1 n for dark mode or CSI ? 997 ; 2 n for light mode.
|
||||
*/
|
||||
private _reportColorScheme(): void {
|
||||
if (!this._themeService) return;
|
||||
const bgLuminance = rgb.relativeLuminance(this._themeService.colors.background.rgba >> 8);
|
||||
const fgLuminance = rgb.relativeLuminance(this._themeService.colors.foreground.rgba >> 8);
|
||||
// Dark mode = background is darker than foreground (lower luminance)
|
||||
const colorSchemeMode = bgLuminance < fgLuminance ? 1 : 2;
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}[?997;${colorSchemeMode}n`);
|
||||
}
|
||||
|
||||
protected _setup(): void {
|
||||
super._setup();
|
||||
|
||||
@@ -495,6 +509,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._themeService = this._instantiationService.createInstance(ThemeService);
|
||||
this._instantiationService.setService(IThemeService, this._themeService);
|
||||
|
||||
// CSI ? 996 n - color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)
|
||||
this._register(this._inputHandler.onRequestColorSchemeQuery(() => this._reportColorScheme()));
|
||||
|
||||
// Emit unsolicited color scheme notification on theme change when DECSET 2031 is enabled
|
||||
this._register(this._themeService.onChangeColors(() => {
|
||||
if (this.coreService.decPrivateModes.colorSchemeUpdates) {
|
||||
this._reportColorScheme();
|
||||
}
|
||||
}));
|
||||
|
||||
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
|
||||
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
|
||||
|
||||
@@ -566,7 +590,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this.textarea!.focus();
|
||||
this.textarea!.select();
|
||||
}));
|
||||
this._register(Event.any(
|
||||
this._register(EventUtils.any(
|
||||
this._onScroll.event,
|
||||
this._inputHandler.onScroll
|
||||
)(() => {
|
||||
@@ -739,11 +763,12 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
return this.cancel(ev);
|
||||
},
|
||||
wheel: (ev: WheelEvent) => {
|
||||
sendEvent(ev);
|
||||
return this.cancel(ev, true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
mousedrag: (ev: MouseEvent) => {
|
||||
// deal only with move while a button is held
|
||||
@@ -793,15 +818,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
if (!(events & CoreMouseEventType.UP)) {
|
||||
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
|
||||
requestedEvents.mouseup = null;
|
||||
} else if (!requestedEvents.mouseup) {
|
||||
requestedEvents.mouseup = eventListeners.mouseup;
|
||||
} else {
|
||||
requestedEvents.mouseup ??= eventListeners.mouseup;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.DRAG)) {
|
||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!);
|
||||
requestedEvents.mousedrag = null;
|
||||
} else if (!requestedEvents.mousedrag) {
|
||||
requestedEvents.mousedrag = eventListeners.mousedrag;
|
||||
} else {
|
||||
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
||||
}
|
||||
}));
|
||||
// force initial onProtocolChange so we dont miss early mouse requests
|
||||
@@ -843,8 +868,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
if (requestedEvents.mousedrag) {
|
||||
this._document!.addEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
|
||||
return this.cancel(ev);
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(el, 'wheel', (ev: WheelEvent) => {
|
||||
@@ -875,13 +898,17 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
self._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
return this.cancel(ev, true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct and send sequences
|
||||
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||
this.coreService.triggerDataEvent(sequence, true);
|
||||
return this.cancel(ev, true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}, { passive: false }));
|
||||
}
|
||||
@@ -1091,7 +1118,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) {
|
||||
const scrollCount = this.rows - 1;
|
||||
this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount);
|
||||
return this.cancel(event, true);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.type === KeyboardResultType.SELECT_ALL) {
|
||||
@@ -1104,7 +1133,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
if (result.cancel) {
|
||||
// The event is canceled at the end already, is this necessary?
|
||||
this.cancel(event, true);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (!result.key) {
|
||||
@@ -1132,16 +1162,19 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this.textarea!.value = '';
|
||||
}
|
||||
|
||||
const wasModifierOnly = this._keyboardService.useWin32InputMode && wasModifierKeyOnlyEvent(event);
|
||||
this._onKey.fire({ key: result.key, domEvent: event });
|
||||
this._showCursor();
|
||||
this.coreService.triggerDataEvent(result.key, true);
|
||||
this.coreService.triggerDataEvent(result.key, !wasModifierOnly);
|
||||
|
||||
// Cancel events when not in screen reader mode so events don't get bubbled up and handled by
|
||||
// other listeners. When screen reader mode is enabled, we don't cancel them (unless ctrl or alt
|
||||
// is also depressed) so that the cursor textarea can be updated, which triggers the screen
|
||||
// reader to read it.
|
||||
if (!this.optionsService.rawOptions.screenReaderMode || event.altKey || event.ctrlKey) {
|
||||
return this.cancel(event, true);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
this._keyDownHandled = true;
|
||||
@@ -1175,7 +1208,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
// Handle key release for Kitty keyboard protocol
|
||||
const result = this._keyboardService.evaluateKeyUp(ev);
|
||||
if (result?.key) {
|
||||
this.coreService.triggerDataEvent(result.key, true);
|
||||
const wasModifierOnly = this._keyboardService.useWin32InputMode && wasModifierKeyOnlyEvent(ev);
|
||||
this.coreService.triggerDataEvent(result.key, !wasModifierOnly);
|
||||
}
|
||||
|
||||
this.updateCursorStyle(ev);
|
||||
@@ -1201,8 +1235,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cancel(ev);
|
||||
|
||||
if (ev.charCode) {
|
||||
key = ev.charCode;
|
||||
} else if (ev.which === null || ev.which === undefined) {
|
||||
@@ -1255,8 +1287,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
const text = ev.data;
|
||||
this.coreService.triggerDataEvent(text, true);
|
||||
|
||||
this.cancel(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1368,15 +1398,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove cancel function and cancelEvents option
|
||||
public cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | undefined {
|
||||
if (!this.options.cancelEvents && !force) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1386,5 +1407,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
|
||||
return ev.keyCode === 16 || // Shift
|
||||
ev.keyCode === 17 || // Ctrl
|
||||
ev.keyCode === 18; // Alt
|
||||
ev.keyCode === 18 || // Alt
|
||||
ev.keyCode === 91 || // Meta (Left)
|
||||
ev.keyCode === 92 || // Meta (Right)
|
||||
ev.keyCode === 93 || // Meta (Menu)
|
||||
ev.keyCode === 224 || // Meta (Firefox)
|
||||
ev.key === 'Meta';
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
*/
|
||||
|
||||
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
||||
import { Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, dispose, toDisposable } from 'common/Lifecycle';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Emitter } from 'common/Event';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
|
||||
export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
|
||||
@@ -9,6 +9,8 @@ import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
|
||||
|
||||
export class OscLinkProvider implements ILinkProvider {
|
||||
private readonly _workCell = new CellData();
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@@ -25,7 +27,7 @@ export class OscLinkProvider implements ILinkProvider {
|
||||
|
||||
const result: ILink[] = [];
|
||||
const linkHandler = this._optionsService.rawOptions.linkHandler;
|
||||
const cell = new CellData();
|
||||
const cell = this._workCell;
|
||||
const lineLength = line.getTrimmedLength();
|
||||
let currentLinkId = -1;
|
||||
let currentStart = -1;
|
||||
|
||||
@@ -40,8 +40,8 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||
this._rowCount = rowCount;
|
||||
// Get the min/max row start/end for the arg values
|
||||
rowStart = rowStart !== undefined ? rowStart : 0;
|
||||
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
|
||||
rowStart = rowStart ?? 0;
|
||||
rowEnd = rowEnd ?? this._rowCount - 1;
|
||||
// Set the properties to the updated values
|
||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
|
||||
@@ -190,9 +190,7 @@ describe('Terminal', () => {
|
||||
});
|
||||
it('should clear a buffer larger than rows', async () => {
|
||||
// Fill the buffer with dummy rows
|
||||
for (let i = 0; i < term.rows * 2; i++) {
|
||||
await term.writeP('test\n');
|
||||
}
|
||||
await term.writeP('test\n'.repeat(term.rows * 2));
|
||||
|
||||
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
||||
term.clear();
|
||||
@@ -389,9 +387,7 @@ describe('Terminal', () => {
|
||||
|
||||
it('should not scroll down, when a custom keydown handler prevents the event', async () => {
|
||||
// Add some output to the terminal
|
||||
for (let i = 0; i < term.rows * 3; i++) {
|
||||
await term.writeP('test\r\n');
|
||||
}
|
||||
await term.writeP('test\r\n'.repeat(term.rows * 3));
|
||||
const startYDisp = (term.rows * 2) + 1;
|
||||
term.attachCustomKeyEventHandler(() => {
|
||||
return false;
|
||||
@@ -405,6 +401,42 @@ describe('Terminal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyDown', () => {
|
||||
it('should not scroll down on modifier-only input in win32 input mode', async () => {
|
||||
term.options.vtExtensions = { win32InputMode: true };
|
||||
term.coreService.decPrivateModes.win32InputMode = true;
|
||||
(term as any).textarea = { value: '' };
|
||||
|
||||
await term.writeP('test\r\n'.repeat(term.rows * 3));
|
||||
const startYDisp = term.buffer.ydisp;
|
||||
term.scrollLines(-1);
|
||||
const scrolledYDisp = term.buffer.ydisp;
|
||||
assert.equal(scrolledYDisp, startYDisp - 1);
|
||||
|
||||
const evKeyDown = {
|
||||
type: 'keydown',
|
||||
key: 'Control',
|
||||
keyCode: 17,
|
||||
ctrlKey: true,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
} as KeyboardEvent;
|
||||
|
||||
const evKeyUp = {
|
||||
type: 'keyup',
|
||||
key: 'Control',
|
||||
keyCode: 17,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
} as KeyboardEvent;
|
||||
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal(term.buffer.ydisp, scrolledYDisp);
|
||||
(term as any)._keyUp(evKeyUp);
|
||||
assert.equal(term.buffer.ydisp, scrolledYDisp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scroll() function', () => {
|
||||
describe('when scrollback > 0', () => {
|
||||
it('should create a new line and scroll', () => {
|
||||
@@ -731,70 +763,87 @@ describe('Terminal', () => {
|
||||
it(`${range}: 2 characters per cell`, async function (): Promise<void> {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
const values: string[] = [];
|
||||
for (let j = i; j <= i + 0xF; j++) {
|
||||
await term.writeP(high + String.fromCharCode(j));
|
||||
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
|
||||
assert.equal(tchar.getChars(), high + String.fromCharCode(j));
|
||||
values.push(high + String.fromCharCode(j));
|
||||
}
|
||||
await term.writeP(values.join('\r\n'));
|
||||
for (let idx = 0; idx < values.length; idx++) {
|
||||
const expected = values[idx];
|
||||
const tchar = term.buffer.lines.get(idx)!.loadCell(0, cell);
|
||||
assert.equal(tchar.getChars(), expected);
|
||||
assert.equal(tchar.getChars().length, 2);
|
||||
assert.equal(tchar.getWidth(), 1);
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
|
||||
term.reset();
|
||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(1, cell).getChars(), '');
|
||||
}
|
||||
});
|
||||
it(`${range}: 2 characters at last cell`, async () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
term.buffer.x = term.cols - 1;
|
||||
const values: string[] = [];
|
||||
for (let j = i; j <= i + 0xF; j++) {
|
||||
await term.writeP(high + String.fromCharCode(j));
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(j));
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2);
|
||||
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), '');
|
||||
term.reset();
|
||||
values.push(high + String.fromCharCode(j));
|
||||
}
|
||||
await term.writeP(values.map((value, idx) => `\x1b[${idx + 1};${term.cols}H${value}`).join(''));
|
||||
for (let idx = 0; idx < values.length; idx++) {
|
||||
const expected = values[idx];
|
||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars(), expected);
|
||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars().length, 2);
|
||||
assert.equal(term.buffer.lines.get(idx + 1)!.loadCell(0, cell).getChars(), '');
|
||||
}
|
||||
});
|
||||
it(`${range}: 2 characters per cell over line end with autowrap`, async function (): Promise<void> {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
term.resize(term.cols, 40);
|
||||
const values: string[] = [];
|
||||
for (let j = i; j <= i + 0xF; j++) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
await term.writeP('a' + high + String.fromCharCode(j));
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(j));
|
||||
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2);
|
||||
assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
|
||||
term.reset();
|
||||
values.push(high + String.fromCharCode(j));
|
||||
}
|
||||
await term.writeP(values.map((value, idx) => `\x1b[${idx * 2 + 1};${term.cols}H` + 'a' + value).join(''));
|
||||
for (let idx = 0; idx < values.length; idx++) {
|
||||
const expected = values[idx];
|
||||
const row = idx * 2;
|
||||
assert.equal(term.buffer.lines.get(row)!.loadCell(term.cols - 1, cell).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(row + 1)!.loadCell(0, cell).getChars(), expected);
|
||||
assert.equal(term.buffer.lines.get(row + 1)!.loadCell(0, cell).getChars().length, 2);
|
||||
assert.equal(term.buffer.lines.get(row + 1)!.loadCell(1, cell).getChars(), '');
|
||||
}
|
||||
});
|
||||
it(`${range}: 2 characters per cell over line end without autowrap`, async function (): Promise<void> {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
const values: string[] = [];
|
||||
for (let j = i; j <= i + 0xF; j++) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
await term.writeP('\x1b[?7l'); // Disable wraparound mode
|
||||
const width = wcwidth((0xD800 - 0xD800) * 0x400 + j - 0xDC00 + 0x10000);
|
||||
if (width !== 1) {
|
||||
continue;
|
||||
}
|
||||
await term.writeP('a' + high + String.fromCharCode(j));
|
||||
// auto wraparound mode should cut off the rest of the line
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(j));
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2);
|
||||
assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
|
||||
term.reset();
|
||||
values.push(high + String.fromCharCode(j));
|
||||
}
|
||||
await term.writeP('\x1b[?7l' + values.map((value, idx) => `\x1b[${idx + 1};${term.cols}H` + 'a' + value).join(''));
|
||||
for (let idx = 0; idx < values.length; idx++) {
|
||||
const expected = values[idx];
|
||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars(), expected);
|
||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars().length, 2);
|
||||
assert.equal(term.buffer.lines.get(idx + 1)!.loadCell(1, cell).getChars(), '');
|
||||
}
|
||||
});
|
||||
it(`${range}: splitted surrogates`, async function (): Promise<void> {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
const values: string[] = [];
|
||||
for (let j = i; j <= i + 0xF; j++) {
|
||||
await term.writeP(high + String.fromCharCode(j));
|
||||
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
|
||||
assert.equal(tchar.getChars(), high + String.fromCharCode(j));
|
||||
values.push(high + String.fromCharCode(j));
|
||||
}
|
||||
await term.writeP(values.join('\r\n'));
|
||||
for (let idx = 0; idx < values.length; idx++) {
|
||||
const expected = values[idx];
|
||||
const tchar = term.buffer.lines.get(idx)!.loadCell(0, cell);
|
||||
assert.equal(tchar.getChars(), expected);
|
||||
assert.equal(tchar.getChars().length, 2);
|
||||
assert.equal(tchar.getWidth(), 1);
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
|
||||
term.reset();
|
||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(1, cell).getChars(), '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { css } from 'common/Color';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { Emitter, type Event } from 'vs/base/common/event';
|
||||
import { Emitter, type IEvent } from 'common/Event';
|
||||
|
||||
export class TestTerminal extends CoreBrowserTerminal {
|
||||
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
||||
@@ -30,24 +30,24 @@ export class TestTerminal extends CoreBrowserTerminal {
|
||||
}
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
public onBlur!: Event<void>;
|
||||
public onFocus!: Event<void>;
|
||||
public onA11yChar!: Event<string>;
|
||||
public onWriteParsed!: Event<void>;
|
||||
public onA11yTab!: Event<number>;
|
||||
public onCursorMove!: Event<void>;
|
||||
public onLineFeed!: Event<void>;
|
||||
public onSelectionChange!: Event<void>;
|
||||
public onData!: Event<string>;
|
||||
public onBinary!: Event<string>;
|
||||
public onTitleChange!: Event<string>;
|
||||
public onBell!: Event<void>;
|
||||
public onScroll!: Event<number>;
|
||||
public onWillOpen!: Event<HTMLElement>;
|
||||
public onKey!: Event<{ key: string, domEvent: KeyboardEvent }>;
|
||||
public onRender!: Event<{ start: number, end: number }>;
|
||||
public onResize!: Event<{ cols: number, rows: number }>;
|
||||
public onDimensionsChange!: Event<IRenderDimensionsApi>;
|
||||
public onBlur!: IEvent<void>;
|
||||
public onFocus!: IEvent<void>;
|
||||
public onA11yChar!: IEvent<string>;
|
||||
public onWriteParsed!: IEvent<void>;
|
||||
public onA11yTab!: IEvent<number>;
|
||||
public onCursorMove!: IEvent<void>;
|
||||
public onLineFeed!: IEvent<void>;
|
||||
public onSelectionChange!: IEvent<void>;
|
||||
public onData!: IEvent<string>;
|
||||
public onBinary!: IEvent<string>;
|
||||
public onTitleChange!: IEvent<string>;
|
||||
public onBell!: IEvent<void>;
|
||||
public onScroll!: IEvent<number>;
|
||||
public onWillOpen!: IEvent<HTMLElement>;
|
||||
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
public onRender!: IEvent<{ start: number, end: number }>;
|
||||
public onResize!: IEvent<{ cols: number, rows: number }>;
|
||||
public onDimensionsChange!: IEvent<IRenderDimensionsApi>;
|
||||
public dimensions: IRenderDimensionsApi | undefined;
|
||||
public markers!: IMarker[];
|
||||
public linkifier: ILinkifier2 | undefined;
|
||||
@@ -107,6 +107,9 @@ export class MockTerminal implements ITerminal {
|
||||
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -195,9 +198,6 @@ export class MockTerminal implements ITerminal {
|
||||
public scrollToRow(absoluteRow: number): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public log(text: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -273,9 +273,9 @@ export class MockBuffer implements IBuffer {
|
||||
}
|
||||
|
||||
export class MockRenderer implements IRenderer {
|
||||
public onRequestRedraw!: Event<IRequestRedrawEvent>;
|
||||
public onCanvasResize!: Event<{ width: number, height: number }>;
|
||||
public onRender!: Event<{ start: number, end: number }>;
|
||||
public onRequestRedraw!: IEvent<IRequestRedrawEvent>;
|
||||
public onCanvasResize!: IEvent<{ width: number, height: number }>;
|
||||
public onRender!: IEvent<{ start: number, end: number }>;
|
||||
public dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -378,7 +378,7 @@ export class MockCoreBrowserService implements ICoreBrowserService {
|
||||
export class MockCharSizeService implements ICharSizeService {
|
||||
public serviceBrand: undefined;
|
||||
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
||||
public onCharSizeChange: Event<void> = new Emitter<void>().event;
|
||||
public onCharSizeChange: IEvent<void> = new Emitter<void>().event;
|
||||
constructor(public width: number, public height: number) {}
|
||||
public measure(): void {}
|
||||
}
|
||||
@@ -396,10 +396,10 @@ export class MockMouseService implements IMouseService {
|
||||
|
||||
export class MockRenderService implements IRenderService {
|
||||
public serviceBrand: undefined;
|
||||
public onDimensionsChange: Event<IRenderDimensions> = new Emitter<IRenderDimensions>().event;
|
||||
public onRenderedViewportChange: Event<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event;
|
||||
public onRender: Event<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event;
|
||||
public onRefreshRequest: Event<{ start: number, end: number}> = new Emitter<{ start: number, end: number }>().event;
|
||||
public onDimensionsChange: IEvent<IRenderDimensions> = new Emitter<IRenderDimensions>().event;
|
||||
public onRenderedViewportChange: IEvent<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event;
|
||||
public onRender: IEvent<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event;
|
||||
public onRefreshRequest: IEvent<{ start: number, end: number}> = new Emitter<{ start: number, end: number }>().event;
|
||||
public dimensions: IRenderDimensions = createRenderDimensions();
|
||||
public refreshRows(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
|
||||
@@ -37,8 +37,8 @@ export class TimeBasedDebouncer implements IRenderDebouncer {
|
||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||
this._rowCount = rowCount;
|
||||
// Get the min/max row start/end for the arg values
|
||||
rowStart = rowStart !== undefined ? rowStart : 0;
|
||||
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
|
||||
rowStart = rowStart ?? 0;
|
||||
rowEnd = rowEnd ?? this._rowCount - 1;
|
||||
// Set the properties to the updated values
|
||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
|
||||
+10
-12
@@ -7,7 +7,7 @@ import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types'
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm';
|
||||
import { channels, css } from 'common/Color';
|
||||
import type { Event } from 'vs/base/common/event';
|
||||
import type { IEvent } from 'common/Event';
|
||||
|
||||
/**
|
||||
* A portion of the public API that are implemented identially internally and simply passed through.
|
||||
@@ -23,14 +23,12 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
|
||||
|
||||
readonly dimensions: IRenderDimensionsApi | undefined;
|
||||
|
||||
onBlur: Event<void>;
|
||||
onFocus: Event<void>;
|
||||
onDimensionsChange: Event<IRenderDimensionsApi>;
|
||||
onA11yChar: Event<string>;
|
||||
onA11yTab: Event<number>;
|
||||
onWillOpen: Event<HTMLElement>;
|
||||
|
||||
cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | void;
|
||||
onBlur: IEvent<void>;
|
||||
onFocus: IEvent<void>;
|
||||
onDimensionsChange: IEvent<IRenderDimensionsApi>;
|
||||
onA11yChar: IEvent<string>;
|
||||
onA11yTab: IEvent<number>;
|
||||
onWillOpen: IEvent<HTMLElement>;
|
||||
}
|
||||
|
||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
@@ -101,7 +99,7 @@ export interface IPartialColorSet {
|
||||
|
||||
export interface IViewport extends IDisposable {
|
||||
scrollBarWidth: number;
|
||||
readonly onRequestScrollLines: Event<{ amount: number, suppressScrollEvent: boolean }>;
|
||||
readonly onRequestScrollLines: IEvent<{ amount: number, suppressScrollEvent: boolean }>;
|
||||
syncScrollArea(immediate?: boolean, force?: boolean): void;
|
||||
getLinesScrolled(ev: WheelEvent): number;
|
||||
getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement };
|
||||
@@ -131,8 +129,8 @@ export interface ILinkWithState {
|
||||
}
|
||||
|
||||
export interface ILinkifier2 extends IDisposable {
|
||||
onShowLinkUnderline: Event<ILinkifierEvent>;
|
||||
onHideLinkUnderline: Event<ILinkifierEvent>;
|
||||
onShowLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
onHideLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
readonly currentLink: ILinkWithState | undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
|
||||
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { ViewportConstants } from 'browser/shared/Constants';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
|
||||
import { CoreMouseEventType } from 'common/Types';
|
||||
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
|
||||
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Emitter, EventUtils } from 'common/Event';
|
||||
import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable';
|
||||
import { Gesture, EventType as GestureEventType, type GestureEvent } from 'vs/base/browser/touch';
|
||||
|
||||
@@ -71,7 +71,7 @@ export class Viewport extends Disposable {
|
||||
}));
|
||||
|
||||
this._scrollableElement.setScrollDimensions({ height: 0, scrollHeight: 0 });
|
||||
this._register(Event.runAndSubscribe(themeService.onChangeColors, () => {
|
||||
this._register(EventUtils.runAndSubscribe(themeService.onChangeColors, () => {
|
||||
element.style.backgroundColor = themeService.colors.background.css;
|
||||
this._scrollableElement.getDomNode().style.backgroundColor = themeService.colors.background.css;
|
||||
}));
|
||||
@@ -81,7 +81,7 @@ export class Viewport extends Disposable {
|
||||
this._styleElement = coreBrowserService.mainDocument.createElement('style');
|
||||
screenElement.appendChild(this._styleElement);
|
||||
this._register(toDisposable(() => this._styleElement.remove()));
|
||||
this._register(Event.runAndSubscribe(themeService.onChangeColors, () => {
|
||||
this._register(EventUtils.runAndSubscribe(themeService.onChangeColors, () => {
|
||||
this._styleElement.textContent = [
|
||||
`.xterm .xterm-scrollable-element > .scrollbar > .slider {`,
|
||||
` background: ${themeService.colors.scrollbarSliderBackground.css};`,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
|
||||
|
||||
export class BufferDecorationRenderer extends Disposable {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
|
||||
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
const enum Constants {
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
import * as Strings from 'browser/LocalizableStrings';
|
||||
import { CoreBrowserTerminal as TerminalCore } from 'browser/CoreBrowserTerminal';
|
||||
import { IBufferRange, ITerminal } from 'browser/Types';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ITerminalOptions } from 'common/Types';
|
||||
import { AddonManager } from 'common/public/AddonManager';
|
||||
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
|
||||
import { ParserApi } from 'common/public/ParserApi';
|
||||
import { UnicodeApi } from 'common/public/UnicodeApi';
|
||||
import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, IRenderDimensions, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
|
||||
import type { Event } from 'vs/base/common/event';
|
||||
import type { IEvent } from 'common/Event';
|
||||
|
||||
/**
|
||||
* The set of options that only have an effect when set in the Terminal constructor.
|
||||
@@ -68,26 +68,23 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
}
|
||||
}
|
||||
|
||||
public get onBell(): Event<void> { return this._core.onBell; }
|
||||
public get onBinary(): Event<string> { return this._core.onBinary; }
|
||||
public get onCursorMove(): Event<void> { return this._core.onCursorMove; }
|
||||
public get onData(): Event<string> { return this._core.onData; }
|
||||
public get onKey(): Event<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
|
||||
public get onLineFeed(): Event<void> { return this._core.onLineFeed; }
|
||||
public get onRender(): Event<{ start: number, end: number }> { return this._core.onRender; }
|
||||
public get onResize(): Event<{ cols: number, rows: number }> { return this._core.onResize; }
|
||||
public get onScroll(): Event<number> { return this._core.onScroll; }
|
||||
public get onSelectionChange(): Event<void> { return this._core.onSelectionChange; }
|
||||
public get onTitleChange(): Event<string> { return this._core.onTitleChange; }
|
||||
public get onWriteParsed(): Event<void> { return this._core.onWriteParsed; }
|
||||
public get onDimensionsChange(): Event<IRenderDimensions> { return this._core.onDimensionsChange; }
|
||||
public get onBell(): IEvent<void> { return this._core.onBell; }
|
||||
public get onBinary(): IEvent<string> { return this._core.onBinary; }
|
||||
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
|
||||
public get onData(): IEvent<string> { return this._core.onData; }
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
|
||||
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
|
||||
public get onScroll(): IEvent<number> { return this._core.onScroll; }
|
||||
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
|
||||
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
|
||||
public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
|
||||
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._core.onDimensionsChange; }
|
||||
|
||||
public get element(): HTMLElement | undefined { return this._core.element; }
|
||||
public get parser(): IParser {
|
||||
if (!this._parser) {
|
||||
this._parser = new ParserApi(this._core);
|
||||
}
|
||||
return this._parser;
|
||||
return this._parser ??= new ParserApi(this._core);
|
||||
}
|
||||
public get unicode(): IUnicodeHandling {
|
||||
this._checkProposedApi();
|
||||
@@ -97,10 +94,7 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
public get rows(): number { return this._core.rows; }
|
||||
public get cols(): number { return this._core.cols; }
|
||||
public get buffer(): IBufferNamespaceApi {
|
||||
if (!this._buffer) {
|
||||
this._buffer = this._register(new BufferNamespaceApi(this._core));
|
||||
}
|
||||
return this._buffer;
|
||||
return this._buffer ??= this._register(new BufferNamespaceApi(this._core));
|
||||
}
|
||||
public get markers(): ReadonlyArray<IMarker> {
|
||||
return this._core.markers;
|
||||
|
||||
@@ -12,9 +12,9 @@ import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderMode
|
||||
import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import { color } from 'common/Color';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Emitter } from 'common/Event';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
|
||||
|
||||
|
||||
@@ -397,7 +397,7 @@ export class DomRendererRowFactory {
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
resolvedBg = channels.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
|
||||
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
|
||||
this._addStyle(charElement, `background-color:#${(bg >>> 0).toString(16).padStart(6, '0')}`);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
@@ -434,7 +434,7 @@ export class DomRendererRowFactory {
|
||||
(fg ) & 0xFF
|
||||
);
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, color, cell, bgOverride, fgOverride)) {
|
||||
this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
|
||||
this._addStyle(charElement, `color:#${fg.toString(16).padStart(6, '0')}`);
|
||||
}
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
@@ -494,8 +494,8 @@ export class DomRendererRowFactory {
|
||||
// Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from
|
||||
// non-dim cells
|
||||
const ratio = this._optionsService.rawOptions.minimumContrastRatio / (cell.isDim() ? 2 : 1);
|
||||
adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, ratio);
|
||||
cache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
|
||||
adjustedColor = color.ensureContrastRatio(bgOverride ?? bg, fgOverride ?? fg, ratio);
|
||||
cache.setColor((bgOverride ?? bg).rgba, (fgOverride ?? fg).rgba, adjustedColor ?? null);
|
||||
}
|
||||
|
||||
if (adjustedColor) {
|
||||
@@ -537,10 +537,3 @@ export class DomRendererRowFactory {
|
||||
(start[1] < end[1] && y === start[1] && x >= start[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function padStart(text: string, padChar: string, length: number): string {
|
||||
while (text.length < length) {
|
||||
text = padChar + text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('RendererUtils', () => {
|
||||
line = 2;
|
||||
variantOffset = 0;
|
||||
cells = [cellWidth, cellWidth, doubleCellWidth, doubleCellWidth];
|
||||
result = [3, 2, 0 ,2];
|
||||
result = [3, 2, 0, 2];
|
||||
for (let index = 0; index < cells.length; index++) {
|
||||
const cell = cells[index];
|
||||
variantOffset = computeNextVariantOffset(cell, line, variantOffset);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import type { Event } from 'vs/base/common/event';
|
||||
import type { IEvent } from 'common/Event';
|
||||
|
||||
export interface IDimensions {
|
||||
width: number;
|
||||
@@ -57,7 +57,7 @@ export interface IRenderer extends IDisposable {
|
||||
* Fires when the renderer is requesting to be redrawn on the next animation
|
||||
* frame but is _not_ a result of content changing (eg. selection changes).
|
||||
*/
|
||||
readonly onRequestRedraw: Event<IRequestRedrawEvent>;
|
||||
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
||||
|
||||
dispose(): void;
|
||||
handleDevicePixelRatioChange(): void;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Emitter } from 'common/Event';
|
||||
|
||||
export class CharSizeService extends Disposable implements ICharSizeService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { ICoreBrowserService } from './Services';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Emitter, EventUtils } from 'common/Event';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
export class CoreBrowserService extends Disposable implements ICoreBrowserService {
|
||||
public serviceBrand: undefined;
|
||||
@@ -29,7 +29,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
|
||||
|
||||
// Monitor device pixel ratio
|
||||
this._register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w)));
|
||||
this._register(Event.forward(this._screenDprMonitor.onDprChange, this._onDprChange));
|
||||
this._register(EventUtils.forward(this._screenDprMonitor.onDprChange, this._onDprChange));
|
||||
|
||||
this._register(addDisposableListener(this._textarea, 'focus', () => this._isFocused = true));
|
||||
this._register(addDisposableListener(this._textarea, 'blur', () => this._isFocused = false));
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import { IKeyboardService } from 'browser/services/Services';
|
||||
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
|
||||
import { evaluateKeyboardEventKitty, KittyKeyboardEventType, KittyKeyboardFlags, shouldUseKittyProtocol } from 'common/input/KittyKeyboard';
|
||||
import { evaluateKeyboardEventWin32 } from 'common/input/Win32InputMode';
|
||||
import { KittyKeyboard, KittyKeyboardEventType, KittyKeyboardFlags } from 'common/input/KittyKeyboard';
|
||||
import { Win32InputMode } from 'common/input/Win32InputMode';
|
||||
import { isMac } from 'common/Platform';
|
||||
import { ICoreService, IOptionsService } from 'common/services/Services';
|
||||
import { IKeyboardResult } from 'common/Types';
|
||||
@@ -14,38 +14,51 @@ import { IKeyboardResult } from 'common/Types';
|
||||
export class KeyboardService implements IKeyboardService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _win32InputMode: Win32InputMode | undefined;
|
||||
private _kittyKeyboard: KittyKeyboard | undefined;
|
||||
|
||||
constructor(
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
}
|
||||
|
||||
private _getWin32InputMode(): Win32InputMode {
|
||||
this._win32InputMode ??= new Win32InputMode();
|
||||
return this._win32InputMode;
|
||||
}
|
||||
|
||||
private _getKittyKeyboard(): KittyKeyboard {
|
||||
this._kittyKeyboard ??= new KittyKeyboard();
|
||||
return this._kittyKeyboard;
|
||||
}
|
||||
|
||||
public evaluateKeyDown(event: KeyboardEvent): IKeyboardResult {
|
||||
// Win32 input mode takes priority (most raw)
|
||||
if (this.useWin32InputMode) {
|
||||
return evaluateKeyboardEventWin32(event, true);
|
||||
return this._getWin32InputMode().evaluateKeyboardEvent(event, true);
|
||||
}
|
||||
const kittyFlags = this._coreService.kittyKeyboard.flags;
|
||||
return this.useKitty
|
||||
? evaluateKeyboardEventKitty(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS)
|
||||
? this._getKittyKeyboard().evaluate(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS)
|
||||
: evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, isMac, this._optionsService.rawOptions.macOptionIsMeta);
|
||||
}
|
||||
|
||||
public evaluateKeyUp(event: KeyboardEvent): IKeyboardResult | undefined {
|
||||
// Win32 input mode sends key up events
|
||||
if (this.useWin32InputMode) {
|
||||
return evaluateKeyboardEventWin32(event, false);
|
||||
return this._getWin32InputMode().evaluateKeyboardEvent(event, false);
|
||||
}
|
||||
const kittyFlags = this._coreService.kittyKeyboard.flags;
|
||||
if (this.useKitty && (kittyFlags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) {
|
||||
return evaluateKeyboardEventKitty(event, kittyFlags, KittyKeyboardEventType.RELEASE);
|
||||
return this._getKittyKeyboard().evaluate(event, kittyFlags, KittyKeyboardEventType.RELEASE);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get useKitty(): boolean {
|
||||
const kittyFlags = this._coreService.kittyKeyboard.flags;
|
||||
return !!(this._optionsService.rawOptions.vtExtensions?.kittyKeyboard && shouldUseKittyProtocol(kittyFlags));
|
||||
return !!(this._optionsService.rawOptions.vtExtensions?.kittyKeyboard && KittyKeyboard.shouldUseProtocol(kittyFlags));
|
||||
}
|
||||
|
||||
public get useWin32InputMode(): boolean {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ILinkProvider, ILinkProviderService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
export class LinkProviderService extends Disposable implements ILinkProviderService {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user