Compare commits

..
168 changed files with 2796 additions and 6427 deletions
+2 -2
View File
@@ -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
@@ -1,18 +0,0 @@
---
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).
+1 -1
View File
@@ -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,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
+1 -1
View File
@@ -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 {
+3 -2
View File
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
+1 -3
View File
@@ -30,9 +30,7 @@ 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
kittySupport: true, // enable Kitty graphics support
kittySizeLimit: 20000000 // size limit of a single Kitty sequence
iipSizeLimit: 20000000 // size limit of a single IIP sequence
}
// initialization
Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

+1 -1
View File
@@ -25,6 +25,6 @@
},
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.3.0"
"xterm-wasm-parts": "^0.1.0"
}
}
+19 -26
View File
@@ -5,16 +5,16 @@
import { IImageAddonOptions, IOscHandler, 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 Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
// 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;
// eslint-disable-next-line
declare const Buffer: any;
// limit hold memory in base64 decoder
const KEEP_DATA = 4194304;
// 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: Base64Decoder;
private _dec = new Base64Decoder(KEEP_DATA);
private _metrics = UNSUPPORTED_TYPE;
constructor(
@@ -39,11 +39,7 @@ 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 {}
@@ -58,7 +54,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
if (this._aborted) return;
if (this._hp.state === HeaderState.END) {
if (this._dec.put(data.subarray(start, end)) !== DECODER_OK) {
if (this._dec.put(data, start, end)) {
this._dec.release();
this._aborted = true;
}
@@ -74,8 +70,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
this._aborted = true;
return;
}
this._dec.init();
if (this._dec.put(data.subarray(dataPos, end)) !== DECODER_OK) {
this._dec.init(this._header.size);
if (this._dec.put(data, dataPos, end)) {
this._dec.release();
this._aborted = true;
}
@@ -93,15 +89,13 @@ export class IIPHandler implements IOscHandler, IResetHandler {
let cond: number | boolean = true;
if (cond = success) {
if (cond = !this._dec.end()) {
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;
}
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;
}
}
}
@@ -111,8 +105,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return true;
}
// 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 });
const blob = new Blob([new Uint8Array(this._dec.data8)], { type: this._metrics.mime });
this._dec.release();
if (!window.createImageBitmap) {
+2 -3
View File
@@ -8,7 +8,6 @@ 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.
@@ -82,7 +81,7 @@ function toName(data: Uint32Array): string {
return new TextDecoder().decode(b);
}
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
inline: toInt,
size: toInt,
name: toName,
@@ -101,7 +100,7 @@ export class HeaderParser {
private _buffer = new Uint32Array(MAX_FIELDCHARS);
private _position = 0;
private _key = '';
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
public fields: {[key: string]: any} = {};
public reset(): void {
this._buffer.fill(0);
+3 -15
View File
@@ -8,7 +8,6 @@ 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';
@@ -23,9 +22,7 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000,
kittySupport: true,
kittySizeLimit: 20000000
iipSizeLimit: 20000000
};
// max palette size supported by the sixel lib (compile time setting)
@@ -51,7 +48,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;
@@ -93,7 +90,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;
@@ -147,15 +144,6 @@ 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.
+2 -2
View File
@@ -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,
+8 -24
View File
@@ -132,10 +132,8 @@ export class ImageStorage implements IDisposable {
) {
try {
this.setLimit(this._opts.storageLimit);
} catch (e: unknown) {
if (e instanceof Error) {
console.error(e.message);
}
} catch (e: any) {
console.error(e.message);
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
}
this._viewportMetrics = {
@@ -234,23 +232,10 @@ export class ImageStorage implements IDisposable {
}
}
/**
* Delete an image by its internal storage ID.
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
*/
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 {
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
// never allow storage to exceed memory limit
this._evictOldest(img.width * img.height);
@@ -344,7 +329,6 @@ export class ImageStorage implements IDisposable {
// finally add the image
this._images.set(imageId, imgSpec);
return imageId;
}
@@ -397,7 +381,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;
@@ -416,7 +400,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)
) {
@@ -458,7 +442,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;
@@ -503,7 +487,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) {
@@ -523,7 +507,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) {
+2 -4
View File
@@ -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, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
import type { IParams, IDcsHandler, IOscHandler, 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, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
/**
* Plugin ctor options.
@@ -38,8 +38,6 @@ export interface IImageAddonOptions {
sixelSizeLimit: number;
iipSupport: boolean;
iipSizeLimit: number;
kittySupport: boolean;
kittySizeLimit: number;
}
export interface IResetHandler {
@@ -1,634 +0,0 @@
/**
* 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;
}
}
@@ -1,90 +0,0 @@
/**
* 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);
});
});
});
@@ -1,171 +0,0 @@
/**
* 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;
}
+2 -8
View File
@@ -23,8 +23,6 @@ export interface IImageAddonOptions {
sixelSizeLimit: number;
iipSupport: boolean;
iipSizeLimit: number;
kittySupport: boolean;
kittySizeLimit: number;
}
// eslint-disable-next-line
@@ -136,9 +134,7 @@ test.describe('ImageAddon', () => {
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000,
kittySupport: true,
kittySizeLimit: 20000000
iipSizeLimit: 20000000
};
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
});
@@ -153,9 +149,7 @@ test.describe('ImageAddon', () => {
storageLimit: 10,
showPlaceholder: false,
iipSupport: false,
iipSizeLimit: 1000,
kittySupport: false,
kittySizeLimit: 1000
iipSizeLimit: 1000
};
await ctx.page.evaluate(opts => {
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
-9
View File
@@ -76,15 +76,6 @@ 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 {
+2 -2
View File
@@ -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 || [
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
+9 -11
View File
@@ -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: unknown) {
} catch (err: any) {
// A `TypeError` indicates the 'local-fonts'
// permission is not yet implemented, so
// only `throw` if this is _not_ the problem.
if (err instanceof Error && err.name !== 'TypeError') {
if (err.name !== 'TypeError') {
throw err;
}
}
@@ -60,10 +60,8 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
fonts[metadata.family].push(metadata);
}
fontsPromise = Promise.resolve(fonts);
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err.name, err.message);
}
} catch (err: any) {
console.error(err.name, err.message);
}
}
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
@@ -78,13 +76,13 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
fonts[metadata.family].push(metadata);
}
fontsPromise = Promise.resolve(fonts);
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err.name, err.message);
}
} catch (err: any) {
console.error(err.name, err.message);
}
}
fontsPromise ??= Promise.resolve({});
if (!fontsPromise) {
fontsPromise = Promise.resolve({});
}
}
const fonts = await fontsPromise;
@@ -52,10 +52,12 @@ export function processLookaheadPosition(
}
processedEntries.add(currentEntry.entry);
currentEntry.entry.forward ??= {
individual: {},
range: []
};
if (!currentEntry.entry.forward) {
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
@@ -95,10 +97,12 @@ export function processBacktrackPosition(
}
processedEntries.add(currentEntry.entry);
currentEntry.entry.reverse ??= {
individual: {},
range: []
};
if (!currentEntry.entry.reverse) {
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,6 +54,9 @@ 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:
return table.substitute[coverageIndex] ?? null;
// eslint-disable-next-line eqeqeq
return table.substitute[coverageIndex] != null
? table.substitute[coverageIndex]
: null;
}
}
+1 -1
View File
@@ -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: unknown = undefined;
let loadError: any | undefined = undefined;
return term.registerCharacterJoiner((text: string): [number, number][] => {
// If the font hasn't been loaded yet, load it and return an empty result
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
+2 -2
View File
@@ -4,7 +4,7 @@
*/
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } 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?: ISearchDecorationOptions, noScroll?: boolean): boolean {
private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean {
if (!this._terminal || !this._decorationManager) {
return false;
}
+3 -1
View File
@@ -198,7 +198,9 @@ export class SearchEngine {
}
}
result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (!result) {
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
}
// Search from startRow - 1 to top
if (!result) {
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
@@ -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>&lt;a>&amp;pi;<\/span><\/div>/g) ?? []).length, 1, output);
assert.equal((output.match(/<div><span>&lt;a>&amp;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);
});
});
});
+18 -4
View File
@@ -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,6 +630,20 @@ 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>';
@@ -666,7 +680,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
(color >> 8) & 255,
(color ) & 255
];
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
}
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
return this._ansiColors[color].css;
@@ -686,7 +700,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
(color >> 8) & 255,
(color ) & 255
];
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
}
// Palette color
return this._ansiColors[color].css;
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
@@ -9,15 +9,19 @@ 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 {
this._provider15 ??= new UnicodeGraphemeProvider(false);
this._provider15Graphemes ??= new UnicodeGraphemeProvider(true);
if (! this._provider15) {
this._provider15 = new UnicodeGraphemeProvider(false);
}
if (! this._provider15Graphemes) {
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: { from(s: string, encoding: string): Uint8Array } | undefined;
declare const Buffer: any;
function _dec(s: string): Uint8Array {
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'base64');
const bs = atob(s);
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
+1 -1
View File
@@ -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,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
+2 -2
View File
@@ -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,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
@@ -3,7 +3,7 @@
* @license MIT
*/
import { toDisposable, IDisposable } from 'common/Lifecycle';
import { toDisposable, IDisposable } from 'vs/base/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
+1 -1
View File
@@ -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 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { Terminal } from '@xterm/xterm';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types';
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
+1 -1
View File
@@ -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 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/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';
+7 -14
View File
@@ -5,7 +5,7 @@
import { IColorContrastCache } from 'browser/Types';
import { DIM_OPACITY, TEXT_BASELINE } from './Constants';
import { tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer';
import { loadCustomGlyphDefinitions, tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer';
import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from './Types';
import { NULL_COLOR, channels, color, rgba } from 'common/Color';
@@ -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 'common/Event';
import { Emitter } from 'vs/base/common/event';
/**
* A shared object which is used to draw nothing for a particular cell.
@@ -100,6 +100,10 @@ export class TextureAtlas implements ITextureAtlas {
alpha: this._config.allowTransparency,
willReadFrequently: true
}));
// Start loading custom glyph definitions asynchronously if custom glyphs are enabled
if (this._config.customGlyphs !== false) {
loadCustomGlyphDefinitions();
}
}
public dispose(): void {
@@ -176,17 +180,6 @@ 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;
@@ -410,7 +403,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);
+3 -3
View File
@@ -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 { IEvent } from 'common/Event';
import type { Event } from 'vs/base/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: IEvent<HTMLCanvasElement>;
onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
onAddTextureAtlasCanvas: Event<HTMLCanvasElement>;
onRemoveTextureAtlasCanvas: Event<HTMLCanvasElement>;
/**
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
+14 -8
View File
@@ -7,14 +7,15 @@ 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 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { getSafariVersion, isSafari } from 'common/Platform';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { IWebGL2RenderingContext } from './Types';
import { WebglRenderer } from './WebglRenderer';
import { Emitter, EventUtils } from 'common/Event';
import { setTraceLogger } from 'common/services/LogService';
import { Emitter, Event } from 'vs/base/common/event';
export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi {
export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi {
private _terminal?: Terminal;
private _renderer?: WebglRenderer;
@@ -65,8 +66,13 @@ 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,
@@ -79,10 +85,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
this._customGlyphs,
this._preserveDrawingBuffer
));
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));
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));
renderService.setRenderer(this._renderer);
this._register(toDisposable(() => {
+4 -4
View File
@@ -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, EventUtils } from 'common/Event';
import { Emitter, Event } from 'vs/base/common/event';
import { addDisposableListener } from 'vs/base/browser/dom';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/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(
EventUtils.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
EventUtils.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
Event.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
Event.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
);
}
this._charAtlas = atlas;
@@ -4,8 +4,38 @@
*/
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphCharacterDefinition, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
/**
* Cached custom glyph definitions. This is loaded dynamically to support code splitting.
*/
let customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefinition | undefined } | undefined;
/**
* Promise that resolves when custom glyph definitions are loaded.
*/
let loadingPromise: Promise<void> | undefined;
/**
* Begins loading the custom glyph definitions. This should be called as early as possible
* to ensure the definitions are ready when needed.
* @returns A promise that resolves when the definitions are loaded.
*/
export function loadCustomGlyphDefinitions(): Promise<void> {
if (customGlyphDefinitions) {
return Promise.resolve();
}
if (!loadingPromise) {
loadingPromise = import('./CustomGlyphDefinitions').then(module => {
customGlyphDefinitions = module.customGlyphDefinitions;
}).catch(err => {
// Reset the loading promise on failure to allow retry
loadingPromise = undefined;
throw err;
});
}
return loadingPromise;
}
/**
* Try drawing a custom block element or box drawing character, returning whether it was
@@ -24,6 +54,11 @@ export function tryDrawCustomGlyph(
devicePixelRatio: number,
backgroundColor?: string
): boolean {
// If definitions are not loaded yet, return false to fall back to font rendering
if (!customGlyphDefinitions) {
return false;
}
const unifiedCharDefinition = customGlyphDefinitions[c];
if (unifiedCharDefinition) {
// Normalize to array for uniform handling
@@ -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 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm';
+3 -2
View File
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chromium',
name: 'ChromeStable',
use: {
browserName: 'chromium'
browserName: 'chromium',
channel: 'chrome'
}
},
{
+15 -4
View File
@@ -26,7 +26,10 @@ const commonOptions = {
target: 'es2021',
sourcemap: true,
treeShaking: true,
splitting: true,
chunkNames: 'chunks/[name]-[hash]',
logLevel: 'warning',
outExtension: { '.js': '.mjs' },
};
/** @type {esbuild.BuildOptions} */
@@ -103,7 +106,8 @@ if (config.addon) {
bundleConfig = {
...bundleConfig,
entryPoints: [`addons/addon-${config.addon}/src/${getAddonEntryPoint(config.addon)}.ts`],
outfile: `addons/addon-${config.addon}/lib/addon-${config.addon}.mjs`,
outdir: `addons/addon-${config.addon}/lib`,
entryNames: `addon-${config.addon}`,
};
outConfig = {
...outConfig,
@@ -128,7 +132,8 @@ if (config.addon) {
bundleConfig = {
...bundleConfig,
entryPoints: [`demo/client/client.ts`],
outfile: 'demo/dist/client-bundle.js',
outdir: 'demo/dist',
entryNames: 'client-bundle',
external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'],
alias: {
// Library ESM imports
@@ -164,6 +169,10 @@ if (config.addon) {
format: 'cjs',
platform: 'node',
external: ['node-pty'],
// Splitting is not compatible with cjs format
splitting: false,
// Override the .mjs extension from commonOptions
outExtension: {},
}
skipOut = true;
skipOutTest = true;
@@ -171,7 +180,8 @@ if (config.addon) {
bundleConfig = {
...bundleConfig,
entryPoints: [`src/headless/public/Terminal.ts`],
outfile: `headless/lib-headless/xterm-headless.mjs`
outdir: `headless/lib-headless`,
entryNames: 'xterm-headless',
};
outConfig = {
...outConfig,
@@ -183,7 +193,8 @@ if (config.addon) {
bundleConfig = {
...bundleConfig,
entryPoints: [`src/browser/public/Terminal.ts`],
outfile: `lib/xterm.mjs`
outdir: `lib`,
entryNames: 'xterm',
};
outConfig = {
...outConfig,
-1
View File
@@ -25,7 +25,6 @@ const addons = [
'clipboard',
'fit',
'image',
'kitty-graphics',
'progress',
'search',
'serialize',
+82 -104
View File
@@ -16,12 +16,9 @@ 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';
@@ -61,11 +58,11 @@ export interface IWindowWithTerminal extends Window {
}
declare let window: IWindowWithTerminal;
let term: Terminal | null;
let protocol: string;
let socketURL: string;
let socket: WebSocket | null;
let pid: string;
let term;
let protocol;
let socketURL;
let socket;
let pid;
let controlBar: ControlBar;
let addonsWindow: AddonsWindow;
let addonSearchWindow: AddonSearchWindow;
@@ -76,7 +73,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 },
@@ -119,8 +116,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 {
@@ -144,7 +141,7 @@ const disposeRecreateButtonHandler: () => void = () => {
if (term) {
term.dispose();
term = null;
(window as any).term = null;
window.term = null;
socket = null;
addons.attach.instance = undefined;
addons.clipboard.instance = undefined;
@@ -157,10 +154,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';
}
};
@@ -168,7 +165,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);
@@ -210,20 +207,17 @@ 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));
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 });
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', 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 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 TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
actionElements = {
findNext: addonSearchWindow.findNextInput,
@@ -235,51 +229,48 @@ if (document.location.pathname === '/test') {
// TODO: Most of below should be encapsulated within windows
paddingElement = styleWindow.paddingElement;
controlBar.setTabVisible('addon-image', !!addons.image.instance);
controlBar.setTabVisible('addon-ligatures', !!addons.ligatures.instance);
controlBar.setTabVisible('addon-progress', !!addons.progress.instance);
controlBar.setTabVisible('addon-webgl', true);
controlBar.setTabVisible('addon-search', true);
controlBar.setTabVisible('addon-serialize', true);
controlBar.setTabVisible('addon-image', true);
controlBar.setTabVisible('addon-web-fonts', true);
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));
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;
@@ -291,7 +282,10 @@ function createTerminal(): Terminal {
buildNumber: 22621
} : undefined,
fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"',
theme: { ...xtermjsTheme }
theme: { ...xtermjsTheme },
vtExtensions: {
win32InputMode: isWindows
}
} as ITerminalOptions);
// Load addons
@@ -299,7 +293,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();
@@ -320,16 +314,14 @@ function createTerminal(): Terminal {
typedTerm.loadAddon(addons.webFonts.instance);
typedTerm.loadAddon(addons.clipboard.instance);
(window as any).term = term; // Expose `term` to window for debugging purposes
window.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 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;
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
fetch(url, { method: 'POST' });
});
@@ -341,7 +333,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();
@@ -350,7 +342,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();
@@ -360,13 +352,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.
@@ -380,9 +372,7 @@ function createTerminal(): Terminal {
if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) {
runFakeTerminal();
} else {
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 res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' });
const processId = await res.text();
pid = processId;
socketURL += processId;
@@ -397,52 +387,52 @@ function createTerminal(): Terminal {
}
function runRealTerminal(): void {
addons.attach.instance = new AttachAddon(socket!);
term!.loadAddon(addons.attach.instance);
(term as any)._initialized = true;
initAddons(term!);
addons.attach.instance = new AttachAddon(socket);
term.loadAddon(addons.attach.instance);
term._initialized = true;
initAddons(term);
}
function runFakeTerminal(): void {
if ((term as any)._initialized) {
if (term._initialized) {
return;
}
(term as any)._initialized = true;
initAddons(term!);
term._initialized = true;
initAddons(term);
(term as any).prompt = () => {
term!.write('\r\n$ ');
term.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 as any).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.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 as any).prompt();
term.prompt();
} else if (ev.keyCode === 8) {
// Do not delete the prompt
if ((term as any)._core.buffer.x > 2) {
term!.write('\b \b');
if (term._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';
}
}
@@ -452,19 +442,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) as AddonType[]).forEach(name => {
Object.keys(addons).forEach((name: AddonType) => {
const addon = addons[name];
const checkbox = document.createElement('input') as HTMLInputElement;
checkbox.type = 'checkbox';
@@ -479,12 +469,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();
@@ -510,15 +500,9 @@ 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 {
@@ -535,12 +519,6 @@ 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;
@@ -612,24 +590,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 as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
term._core._register({ dispose: () => element.removeEventListener(type, handler) });
}
function updateTerminalSize(): void {
const width = optionsWindow.autoResize ? '100%'
: ((term as any).dimensions.css.canvas.width + (term as any)._core.viewport.scrollBarWidth).toString() + 'px';
: (term.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = optionsWindow.autoResize ? '100%'
: ((term as any).dimensions.css.canvas.height).toString() + 'px';
terminalContainer!.style.width = width;
terminalContainer!.style.height = height;
addons.fit.instance!.fit();
: (term.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,15 +5,14 @@
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
@@ -47,36 +46,6 @@ 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 {
@@ -90,106 +59,4 @@ 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) });
}
}
@@ -1,44 +0,0 @@
/**
* 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'));
}
}
@@ -1,135 +0,0 @@
/**
* 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.
@@ -1,52 +0,0 @@
/**
* 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;
+1 -1
View File
@@ -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');
+17 -16
View File
@@ -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,6 +109,7 @@ 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',
@@ -142,7 +143,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 as Record<string, unknown>)[o]) {
switch (typeof this._terminal.options[o]) {
case 'boolean':
booleanOptions.push(o);
break;
@@ -159,25 +160,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 as Record<string, unknown>)[o] ? 'checked' : ''}/> ${o}</label></div>`;
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${this._terminal.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
const checked = (this._terminal.options as Record<string, Record<string, unknown> | undefined>)[parent]?.[prop] ?? false;
const checked = this._terminal.options[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 as Record<string, unknown>)[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[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 as Record<string, unknown>)[o];
const selectedOption = o === 'theme' ? 'xtermjs' : this._terminal.options[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 as Record<string, unknown>)[o]}"/></label></div>`;
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${this._terminal.options[o]}"/></label></div>`;
}
});
html += '</div>';
@@ -189,7 +190,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 as Record<string, unknown>)[o] = input.checked;
this._terminal.options[o] = input.checked;
if (o ==='allowTransparency') {
this._terminal.options.theme = this._getTheme();
this._handlers.updateTerminalContainerBackground();
@@ -200,7 +201,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 as Record<string, unknown>)[parent] = { ...(this._terminal.options as Record<string, Record<string, unknown> | undefined>)[parent], [prop]: input.checked };
this._terminal.options[parent] = { ...this._terminal.options[parent], [prop]: input.checked };
});
});
numberOptions.forEach(o => {
@@ -215,7 +216,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
this._terminal.options.scrollback = parseInt(input.value);
setTimeout(() => this._handlers.updateTerminalSize(), 5);
} else {
(this._terminal.options as Record<string, unknown>)[o] = parseInt(input.value);
this._terminal.options[o] = parseInt(input.value);
}
this._handlers.updateTerminalSize();
});
@@ -224,7 +225,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: unknown = input.value;
let value: any = input.value;
if (o === 'colsRows') {
const m = input.value.match(/^([0-9]+)x([0-9]+)$/);
if (m) {
@@ -238,7 +239,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
} else if (o === 'theme') {
value = this._getTheme();
}
(this._terminal.options as Record<string, unknown>)[o] = value;
this._terminal.options[o] = value;
if (o === 'theme') {
this._handlers.updateTerminalContainerBackground();
}
@@ -258,11 +259,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 = {};
theme = undefined;
break;
case 'xtermjs':
theme = { ...xtermjsTheme };
+1 -1
View File
@@ -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');
+242 -10
View File
@@ -8,6 +8,8 @@ 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';
@@ -64,13 +66,61 @@ 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 {
@@ -89,9 +139,7 @@ export class TestWindow extends BaseWindow implements IControlWindow {
}
dd.appendChild(button);
dl.appendChild(dd);
if (handler) {
button.addEventListener('click', handler);
}
button.addEventListener('click', handler);
}
private _addDdWithCheckbox(dl: HTMLElement, id: string, label: string, title: string, checked: boolean): void {
@@ -111,6 +159,38 @@ 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);
}
}
/**
@@ -385,7 +465,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++) {
@@ -671,6 +751,42 @@ 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 = [];
@@ -748,7 +864,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';
});
@@ -780,17 +896,133 @@ 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;
const decoration = term.registerDecoration({
decorationStressTestDecorations.push(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) });
}
+4 -2
View File
@@ -5,12 +5,14 @@
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 _container: HTMLElement;
private _term: Terminal | undefined;
public build(container: HTMLElement): void {
this._container = container;
@@ -63,7 +65,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._terminal.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
element.addEventListener('click', () => this._term?.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
const desc = document.createElement('span');
desc.textContent = description;
+1 -1
View File
@@ -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');
+5 -5
View File
@@ -6,7 +6,6 @@
"rootDir": ".",
"sourceMap": true,
"baseUrl": ".",
"strict": true,
"paths": {
"@xterm/addon-attach": ["../../addons/addon-attach"],
"@xterm/addon-clipboard": ["../../addons/addon-clipboard"],
@@ -19,11 +18,12 @@
"@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"
]
}
+1 -1
View File
@@ -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' | 'ligatures' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl';
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures';
export interface IDemoAddon<T extends AddonType> {
name: T;
-1
View File
@@ -33,7 +33,6 @@ body {
padding: 4px 10px;
cursor: pointer;
font-size: 12px;
outline-offset: -1px;
}
.banner-tabs button:hover {
background: rgba(255,255,255,0.1);
+1 -1
View File
@@ -23,6 +23,6 @@
<div id="sidebar" class="grid">
</div>
</div>
<script src="dist/client-bundle.js" defer ></script>
<script src="dist/client-bundle.mjs" type="module" defer ></script>
</body>
</html>
+3 -18
View File
@@ -65,8 +65,6 @@ 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',
@@ -79,13 +77,7 @@ function startServer(): void {
useConptyDll: isWindows,
});
// 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);
}
console.log('Created terminal with PID: ' + term.pid);
terminals[term.pid] = term;
unsentOutput[term.pid] = '';
temporaryDisposable[term.pid] = term.onData(function(data) {
@@ -103,17 +95,10 @@ 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];
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.');
}
term.resize(cols, rows);
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
res.end();
});
+1 -1
View File
@@ -18,6 +18,6 @@
</head>
<body id="test">
<div id="terminal-container"></div>
<script src="/dist/client-bundle.js" defer></script>
<script src="/dist/client-bundle.mjs" type="module" defer></script>
</body>
</html>
-9
View File
@@ -35,7 +35,6 @@ 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 }],
@@ -67,7 +66,6 @@ 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',
@@ -142,12 +140,5 @@ 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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@xterm/headless",
"description": "A headless terminal component that runs in Node.js",
"version": "6.0.0",
"version": "5.5.0",
"main": "lib-headless/xterm-headless.js",
"module": "lib-headless/xterm-headless.mjs",
"types": "typings/xterm-headless.d.ts",
+283 -22
View File
@@ -41,7 +41,7 @@
"jsdom": "^27.3.0",
"mocha": "^10.1.0",
"mustache": "^4.2.0",
"node-pty": "^1.2.0-beta.9",
"node-pty": "1.1.0-beta19",
"nyc": "^17.1.0",
"source-map-loader": "^3.0.0",
"source-map-support": "^0.5.20",
@@ -79,15 +79,9 @@
"license": "MIT",
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.3.0"
"xterm-wasm-parts": "^0.1.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",
@@ -1284,6 +1278,102 @@
"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",
@@ -1456,6 +1546,16 @@
"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",
@@ -3411,6 +3511,12 @@
"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",
@@ -4762,12 +4868,54 @@
"node": ">= 0.10"
}
},
"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==",
"node_modules/inwasm": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/inwasm/-/inwasm-0.0.13.tgz",
"integrity": "sha512-gmULhw1wfF3tQ19y0TvcNH6A5jN7IuTD51kbZuy+ittUU59d+ZTQMb53wbGQuciMrledgagL3/ohnjUj5qJikQ==",
"dev": true,
"license": "MIT"
"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"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
@@ -5267,6 +5415,21 @@
"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",
@@ -5715,6 +5878,15 @@
"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",
@@ -5844,12 +6016,11 @@
}
},
"node_modules/node-pty": {
"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==",
"version": "1.1.0-beta19",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta19.tgz",
"integrity": "sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^7.1.0"
}
@@ -6252,6 +6423,12 @@
"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",
@@ -6339,6 +6516,28 @@
"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",
@@ -7332,6 +7531,21 @@
"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",
@@ -7344,6 +7558,19 @@
"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",
@@ -7862,6 +8089,23 @@
"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",
@@ -8205,6 +8449,24 @@
"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",
@@ -8317,13 +8579,12 @@
}
},
"node_modules/xterm-wasm-parts": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.3.0.tgz",
"integrity": "sha512-V/lhvDv2Scov2ukhTNmmur6jMq2DSL8QOdQdXWgfqcAYbUf7bgixwl2sB1gYPGE21NTw2OvbAw+vANRncWzR4w==",
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.1.0.tgz",
"integrity": "sha512-GFE8yNJfdkytGpcsOZhkL3B8XyUqkR/Du3SdxRyFbJg+BBCKm3raaaflgIM4TwNQ2AOLz01BEEuB5FZXx8aNTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"inwasm-runtime": "^0.1.2"
"inwasm": "^0.0.13"
}
},
"node_modules/y18n": {
+4 -5
View File
@@ -47,17 +47,16 @@
"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/ test/",
"lint": "eslint --max-warnings 0 src/ addons/ demo/",
"lint-changes": "node ./bin/lint_changes.js",
"lint-changes-fix": "node ./bin/lint_changes.js --fix",
"lint-fix": "eslint --fix src/ addons/ demo/ test/",
"lint-fix": "eslint --fix src/ addons/ demo/",
"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=Chromium\"",
"test-integration-chromium": "node ./bin/test_integration.js --workers=75% \"--project=ChromeStable\"",
"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",
@@ -104,7 +103,7 @@
"jsdom": "^27.3.0",
"mocha": "^10.1.0",
"mustache": "^4.2.0",
"node-pty": "^1.2.0-beta.9",
"node-pty": "1.1.0-beta19",
"nyc": "^17.1.0",
"source-map-loader": "^3.0.0",
"source-map-support": "^0.5.20",
+1 -1
View File
@@ -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 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IBuffer } from 'common/buffer/Types';
import { IInstantiationService } from 'common/services/Services';
+97 -76
View File
@@ -28,7 +28,7 @@ import { OscLinkProvider } from 'browser/OscLinkProvider';
import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal } from 'browser/Types';
import { Viewport } from 'browser/Viewport';
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
import type { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { IRenderer } from 'browser/renderer/shared/Types';
@@ -42,22 +42,44 @@ 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, rgb } from 'common/Color';
import { channels, color } 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, C1ESCAPED } from 'common/data/EscapeSequences';
import { C0, C1_ESCAPED } 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 type { AccessibilityManager } from './AccessibilityManager';
import { Linkifier } from './Linkifier';
import { Emitter, EventUtils, type IEvent } from 'common/Event';
import { Emitter, Event } from 'vs/base/common/event';
import { addDisposableListener } from 'vs/base/browser/dom';
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
/**
* Lazily loaded AccessibilityManager module.
*/
let accessibilityManagerModule: Promise<typeof import('./AccessibilityManager')> | undefined;
function loadAccessibilityManager(): Promise<typeof import('./AccessibilityManager')> {
if (!accessibilityManagerModule) {
accessibilityManagerModule = import('./AccessibilityManager');
}
return accessibilityManagerModule;
}
/**
* Lazily loaded OverviewRulerRenderer module.
*/
let overviewRulerRendererModule: Promise<typeof import('browser/decorations/OverviewRulerRenderer')> | undefined;
function loadOverviewRulerRenderer(): Promise<typeof import('browser/decorations/OverviewRulerRenderer')> {
if (!overviewRulerRendererModule) {
overviewRulerRendererModule = import('browser/decorations/OverviewRulerRenderer');
}
return overviewRulerRendererModule;
}
export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
public textarea: HTMLTextAreaElement | undefined;
@@ -135,15 +157,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
public readonly onBell = this._onBell.event;
private _onFocus = this._register(new Emitter<void>());
public get onFocus(): IEvent<void> { return this._onFocus.event; }
public get onFocus(): Event<void> { return this._onFocus.event; }
private _onBlur = this._register(new Emitter<void>());
public get onBlur(): IEvent<void> { return this._onBlur.event; }
public get onBlur(): Event<void> { return this._onBlur.event; }
private _onA11yCharEmitter = this._register(new Emitter<string>());
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
public get onA11yChar(): Event<string> { return this._onA11yCharEmitter.event; }
private _onA11yTabEmitter = this._register(new Emitter<number>());
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }
public get onA11yTab(): Event<number> { return this._onA11yTabEmitter.event; }
private _onWillOpen = this._register(new Emitter<HTMLElement>());
public get onWillOpen(): IEvent<HTMLElement> { return this._onWillOpen.event; }
public get onWillOpen(): Event<HTMLElement> { return this._onWillOpen.event; }
private readonly _onDimensionsChange = this._register(new Emitter<IRenderDimensionsApi>());
public readonly onDimensionsChange = this._onDimensionsChange.event;
@@ -187,10 +209,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(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));
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));
// Setup listeners
this._register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
@@ -235,7 +257,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)}${C1ESCAPED.ST}`);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`);
break;
case ColorRequestType.SET:
if (acc === 'ansi') {
@@ -252,20 +274,6 @@ 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();
@@ -291,7 +299,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
private _handleScreenReaderModeOptionChange(value: boolean): void {
if (value) {
if (!this._accessibilityManager.value && this._renderService) {
this._accessibilityManager.value = this._instantiationService.createInstance(AccessibilityManager, this);
loadAccessibilityManager().then(module => {
// Check again after async load in case option changed
if (this.options.screenReaderMode && !this._accessibilityManager.value && this._renderService) {
this._accessibilityManager.value = this._instantiationService.createInstance(module.AccessibilityManager, this);
}
}).catch(() => {
// Failed to load accessibility manager module
});
}
} else {
this._accessibilityManager.clear();
@@ -509,16 +524,6 @@ 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);
@@ -590,7 +595,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this.textarea!.focus();
this.textarea!.select();
}));
this._register(EventUtils.any(
this._register(Event.any(
this._onScroll.event,
this._inputHandler.onScroll
)(() => {
@@ -612,16 +617,34 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to
// ensure the correct order of the dprchange event
this._accessibilityManager.value = this._instantiationService.createInstance(AccessibilityManager, this);
loadAccessibilityManager().then(module => {
if (this.options.screenReaderMode && !this._accessibilityManager.value && this._renderService) {
this._accessibilityManager.value = this._instantiationService.createInstance(module.AccessibilityManager, this);
}
}).catch(() => {
// Failed to load accessibility manager module
});
}
this._register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
if (this.options.overviewRuler.width) {
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
loadOverviewRulerRenderer().then(module => {
if (this.options.overviewRuler.width && !this._overviewRulerRenderer && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(module.OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
}).catch(() => {
// Failed to load overview ruler renderer module
});
}
this.optionsService.onSpecificOptionChange('overviewRuler', value => {
if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
loadOverviewRulerRenderer().then(module => {
if (!this._overviewRulerRenderer && this.options.overviewRuler.width && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(module.OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
}).catch(() => {
// Failed to load overview ruler renderer module
});
}
});
// Measure the character size
@@ -763,12 +786,11 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
}
}
return this.cancel(ev);
},
wheel: (ev: WheelEvent) => {
sendEvent(ev);
ev.preventDefault();
ev.stopPropagation();
return false;
return this.cancel(ev, true);
},
mousedrag: (ev: MouseEvent) => {
// deal only with move while a button is held
@@ -818,15 +840,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
if (!(events & CoreMouseEventType.UP)) {
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
requestedEvents.mouseup = null;
} else {
requestedEvents.mouseup ??= eventListeners.mouseup;
} else if (!requestedEvents.mouseup) {
requestedEvents.mouseup = eventListeners.mouseup;
}
if (!(events & CoreMouseEventType.DRAG)) {
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!);
requestedEvents.mousedrag = null;
} else {
requestedEvents.mousedrag ??= eventListeners.mousedrag;
} else if (!requestedEvents.mousedrag) {
requestedEvents.mousedrag = eventListeners.mousedrag;
}
}));
// force initial onProtocolChange so we dont miss early mouse requests
@@ -868,6 +890,8 @@ 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) => {
@@ -898,17 +922,13 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
self._coreBrowserService?.dpr
);
if (lines === 0) {
ev.preventDefault();
ev.stopPropagation();
return false;
return this.cancel(ev, true);
}
// Construct and send sequences
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
this.coreService.triggerDataEvent(sequence, true);
ev.preventDefault();
ev.stopPropagation();
return false;
return this.cancel(ev, true);
}
}, { passive: false }));
}
@@ -1118,9 +1138,7 @@ 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);
event.preventDefault();
event.stopPropagation();
return false;
return this.cancel(event, true);
}
if (result.type === KeyboardResultType.SELECT_ALL) {
@@ -1133,8 +1151,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
if (result.cancel) {
// The event is canceled at the end already, is this necessary?
event.preventDefault();
event.stopPropagation();
this.cancel(event, true);
}
if (!result.key) {
@@ -1162,19 +1179,16 @@ 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, !wasModifierOnly);
this.coreService.triggerDataEvent(result.key, true);
// 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) {
event.preventDefault();
event.stopPropagation();
return false;
return this.cancel(event, true);
}
this._keyDownHandled = true;
@@ -1208,8 +1222,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
// Handle key release for Kitty keyboard protocol
const result = this._keyboardService.evaluateKeyUp(ev);
if (result?.key) {
const wasModifierOnly = this._keyboardService.useWin32InputMode && wasModifierKeyOnlyEvent(ev);
this.coreService.triggerDataEvent(result.key, !wasModifierOnly);
this.coreService.triggerDataEvent(result.key, true);
}
this.updateCursorStyle(ev);
@@ -1235,6 +1248,8 @@ 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) {
@@ -1287,6 +1302,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
const text = ev.data;
this.coreService.triggerDataEvent(text, true);
this.cancel(ev);
return true;
}
@@ -1398,6 +1415,15 @@ 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;
}
}
/**
@@ -1407,10 +1433,5 @@ 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 === 91 || // Meta (Left)
ev.keyCode === 92 || // Meta (Right)
ev.keyCode === 93 || // Meta (Menu)
ev.keyCode === 224 || // Meta (Firefox)
ev.key === 'Meta';
ev.keyCode === 18; // Alt
}
+2 -2
View File
@@ -4,11 +4,11 @@
*/
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { Disposable, dispose, toDisposable } from 'common/Lifecycle';
import { Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { IDisposable } from 'common/Types';
import { IBufferService } from 'common/services/Services';
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
import { Emitter } from 'common/Event';
import { Emitter } from 'vs/base/common/event';
import { addDisposableListener } from 'vs/base/browser/dom';
export class Linkifier extends Disposable implements ILinkifier2 {
+1 -3
View File
@@ -9,8 +9,6 @@ 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,
@@ -27,7 +25,7 @@ export class OscLinkProvider implements ILinkProvider {
const result: ILink[] = [];
const linkHandler = this._optionsService.rawOptions.linkHandler;
const cell = this._workCell;
const cell = new CellData();
const lineLength = line.getTrimmedLength();
let currentLinkId = -1;
let currentStart = -1;
+2 -2
View File
@@ -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 ?? 0;
rowEnd = rowEnd ?? this._rowCount - 1;
rowStart = rowStart !== undefined ? rowStart : 0;
rowEnd = rowEnd !== undefined ? 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;
+37 -86
View File
@@ -190,7 +190,9 @@ describe('Terminal', () => {
});
it('should clear a buffer larger than rows', async () => {
// Fill the buffer with dummy rows
await term.writeP('test\n'.repeat(term.rows * 2));
for (let i = 0; i < term.rows * 2; i++) {
await term.writeP('test\n');
}
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
term.clear();
@@ -387,7 +389,9 @@ describe('Terminal', () => {
it('should not scroll down, when a custom keydown handler prevents the event', async () => {
// Add some output to the terminal
await term.writeP('test\r\n'.repeat(term.rows * 3));
for (let i = 0; i < term.rows * 3; i++) {
await term.writeP('test\r\n');
}
const startYDisp = (term.rows * 2) + 1;
term.attachCustomKeyEventHandler(() => {
return false;
@@ -401,42 +405,6 @@ 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', () => {
@@ -763,87 +731,70 @@ 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++) {
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);
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));
assert.equal(tchar.getChars().length, 2);
assert.equal(tchar.getWidth(), 1);
assert.equal(term.buffer.lines.get(idx)!.loadCell(1, cell).getChars(), '');
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
it(`${range}: 2 characters at last cell`, async () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
const values: string[] = [];
term.buffer.x = term.cols - 1;
for (let j = i; j <= i + 0xF; j++) {
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(), '');
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();
}
});
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++) {
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(), '');
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();
}
});
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;
}
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(), '');
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();
}
});
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++) {
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);
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));
assert.equal(tchar.getChars().length, 2);
assert.equal(tchar.getWidth(), 1);
assert.equal(term.buffer.lines.get(idx)!.loadCell(1, cell).getChars(), '');
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
}
+30 -30
View File
@@ -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 IEvent } from 'common/Event';
import { Emitter, type Event } from 'vs/base/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!: 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 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 dimensions: IRenderDimensionsApi | undefined;
public markers!: IMarker[];
public linkifier: ILinkifier2 | undefined;
@@ -107,9 +107,6 @@ 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.');
}
@@ -198,6 +195,9 @@ 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!: IEvent<IRequestRedrawEvent>;
public onCanvasResize!: IEvent<{ width: number, height: number }>;
public onRender!: IEvent<{ start: number, end: number }>;
public onRequestRedraw!: Event<IRequestRedrawEvent>;
public onCanvasResize!: Event<{ width: number, height: number }>;
public onRender!: Event<{ 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: IEvent<void> = new Emitter<void>().event;
public onCharSizeChange: Event<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: 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 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 dimensions: IRenderDimensions = createRenderDimensions();
public refreshRows(start: number, end: number): void {
throw new Error('Method not implemented.');
+2 -2
View File
@@ -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 ?? 0;
rowEnd = rowEnd ?? this._rowCount - 1;
rowStart = rowStart !== undefined ? rowStart : 0;
rowEnd = rowEnd !== undefined ? 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;
+12 -10
View File
@@ -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 { IEvent } from 'common/Event';
import type { Event } from 'vs/base/common/event';
/**
* A portion of the public API that are implemented identially internally and simply passed through.
@@ -23,12 +23,14 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
readonly dimensions: IRenderDimensionsApi | undefined;
onBlur: IEvent<void>;
onFocus: IEvent<void>;
onDimensionsChange: IEvent<IRenderDimensionsApi>;
onA11yChar: IEvent<string>;
onA11yTab: IEvent<number>;
onWillOpen: IEvent<HTMLElement>;
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;
}
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -99,7 +101,7 @@ export interface IPartialColorSet {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
readonly onRequestScrollLines: IEvent<{ amount: number, suppressScrollEvent: boolean }>;
readonly onRequestScrollLines: Event<{ amount: number, suppressScrollEvent: boolean }>;
syncScrollArea(immediate?: boolean, force?: boolean): void;
getLinesScrolled(ev: WheelEvent): number;
getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement };
@@ -129,8 +131,8 @@ export interface ILinkWithState {
}
export interface ILinkifier2 extends IDisposable {
onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>;
onShowLinkUnderline: Event<ILinkifierEvent>;
onHideLinkUnderline: Event<ILinkifierEvent>;
readonly currentLink: ILinkWithState | undefined;
}
+4 -4
View File
@@ -5,13 +5,13 @@
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ViewportConstants } from 'browser/shared/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/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, EventUtils } from 'common/Event';
import { Emitter, Event } from 'vs/base/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(EventUtils.runAndSubscribe(themeService.onChangeColors, () => {
this._register(Event.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(EventUtils.runAndSubscribe(themeService.onChangeColors, () => {
this._register(Event.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 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/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 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
const enum Constants {
+23 -17
View File
@@ -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 'common/Lifecycle';
import { Disposable } from 'vs/base/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 { IEvent } from 'common/Event';
import type { Event } from 'vs/base/common/event';
/**
* The set of options that only have an effect when set in the Terminal constructor.
@@ -68,23 +68,26 @@ export class Terminal extends Disposable implements ITerminalApi {
}
}
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 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 element(): HTMLElement | undefined { return this._core.element; }
public get parser(): IParser {
return this._parser ??= new ParserApi(this._core);
if (!this._parser) {
this._parser = new ParserApi(this._core);
}
return this._parser;
}
public get unicode(): IUnicodeHandling {
this._checkProposedApi();
@@ -94,7 +97,10 @@ 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 {
return this._buffer ??= this._register(new BufferNamespaceApi(this._core));
if (!this._buffer) {
this._buffer = this._register(new BufferNamespaceApi(this._core));
}
return this._buffer;
}
public get markers(): ReadonlyArray<IMarker> {
return this._core.markers;
+2 -2
View File
@@ -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 'common/Lifecycle';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event';
import { Emitter } from 'vs/base/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:#${(bg >>> 0).toString(16).padStart(6, '0')}`);
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
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:#${fg.toString(16).padStart(6, '0')}`);
this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
}
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,3 +537,10 @@ 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);
+2 -2
View File
@@ -6,7 +6,7 @@
import { Terminal } from '@xterm/xterm';
import { ITerminal } from 'browser/Types';
import { IDisposable } from 'common/Types';
import type { IEvent } from 'common/Event';
import type { Event } from 'vs/base/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: IEvent<IRequestRedrawEvent>;
readonly onRequestRedraw: Event<IRequestRedrawEvent>;
dispose(): void;
handleDevicePixelRatioChange(): void;

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