diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 461f8fdb..c9954ed7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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-` +- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-` - Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them ## Addon Development Pattern diff --git a/.github/instructions/benchmark.instructions.md b/.github/instructions/benchmark.instructions.md new file mode 100644 index 00000000..2e586708 --- /dev/null +++ b/.github/instructions/benchmark.instructions.md @@ -0,0 +1,18 @@ +--- +applyTo: '**/*.benchmark.ts' +--- +# Benchmark run instructions + +- Full suite: `npm run benchmark` +- Single benchmark file: + - Tree: `npm run benchmark -- -t out-test/benchmark/Event.benchmark.js` + - Run file: `npm run benchmark -- -s "out-test/benchmark/Event.benchmark.js" out-test/benchmark/Event.benchmark.js` +- Single context/case: + - Use `-t` to get the path, then: + - `npm run benchmark -- -s "" 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). diff --git a/.github/instructions/unit-test-instructions.instructions.md b/.github/instructions/unit-test.instructions.md similarity index 100% rename from .github/instructions/unit-test-instructions.instructions.md rename to .github/instructions/unit-test.instructions.md diff --git a/addons/addon-attach/src/AttachAddon.ts b/addons/addon-attach/src/AttachAddon.ts index 37a10d17..9463dc36 100644 --- a/addons/addon-attach/src/AttachAddon.ts +++ b/addons/addon-attach/src/AttachAddon.ts @@ -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[] = []; diff --git a/addons/addon-attach/test/playwright.config.ts b/addons/addon-attach/test/playwright.config.ts index 22834be1..e2fe314e 100644 --- a/addons/addon-attach/test/playwright.config.ts +++ b/addons/addon-attach/test/playwright.config.ts @@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = { timeout: 10000, projects: [ { - name: 'ChromeStable', + name: 'Chromium', use: { - browserName: 'chromium', - channel: 'chrome' + browserName: 'chromium' } }, { diff --git a/addons/addon-clipboard/test/playwright.config.ts b/addons/addon-clipboard/test/playwright.config.ts index 22834be1..e2fe314e 100644 --- a/addons/addon-clipboard/test/playwright.config.ts +++ b/addons/addon-clipboard/test/playwright.config.ts @@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = { timeout: 10000, projects: [ { - name: 'ChromeStable', + name: 'Chromium', use: { - browserName: 'chromium', - channel: 'chrome' + browserName: 'chromium' } }, { diff --git a/addons/addon-fit/src/FitAddon.ts b/addons/addon-fit/src/FitAddon.ts index 23004c1c..3b06fb73 100644 --- a/addons/addon-fit/src/FitAddon.ts +++ b/addons/addon-fit/src/FitAddon.ts @@ -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 { diff --git a/addons/addon-fit/test/playwright.config.ts b/addons/addon-fit/test/playwright.config.ts index 22834be1..e2fe314e 100644 --- a/addons/addon-fit/test/playwright.config.ts +++ b/addons/addon-fit/test/playwright.config.ts @@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = { timeout: 10000, projects: [ { - name: 'ChromeStable', + name: 'Chromium', use: { - browserName: 'chromium', - channel: 'chrome' + browserName: 'chromium' } }, { diff --git a/addons/addon-image/src/IIPHandler.ts b/addons/addon-image/src/IIPHandler.ts index bebfee11..cca3f2ac 100644 --- a/addons/addon-image/src/IIPHandler.ts +++ b/addons/addon-image/src/IIPHandler.ts @@ -9,10 +9,6 @@ import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm'; import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser'; import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics'; - -// eslint-disable-next-line -declare const Buffer: any; - // limit hold memory in base64 decoder const KEEP_DATA = 4194304; @@ -105,7 +101,8 @@ export class IIPHandler implements IOscHandler, IResetHandler { return true; } - const blob = new Blob([new Uint8Array(this._dec.data8)], { type: this._metrics.mime }); + // HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it + const blob = new Blob([this._dec.data8 as Uint8Array], { type: this._metrics.mime }); this._dec.release(); if (!window.createImageBitmap) { diff --git a/addons/addon-image/src/IIPHeaderParser.ts b/addons/addon-image/src/IIPHeaderParser.ts index dd872fed..af4e9a68 100644 --- a/addons/addon-image/src/IIPHeaderParser.ts +++ b/addons/addon-image/src/IIPHeaderParser.ts @@ -8,6 +8,7 @@ declare const Buffer: any; export interface IHeaderFields { + [key: string]: number | string | Uint32Array | null | undefined; // base-64 encoded filename. Defaults to "Unnamed file". name: string; // File size in bytes. The file transfer will be canceled if this size is exceeded. @@ -81,7 +82,7 @@ function toName(data: Uint32Array): string { return new TextDecoder().decode(b); } -const DECODERS: {[key: string]: (v: Uint32Array) => any} = { +const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = { inline: toInt, size: toInt, name: toName, @@ -100,7 +101,7 @@ export class HeaderParser { private _buffer = new Uint32Array(MAX_FIELDCHARS); private _position = 0; private _key = ''; - public fields: {[key: string]: any} = {}; + public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {}; public reset(): void { this._buffer.fill(0); diff --git a/addons/addon-image/src/ImageAddon.ts b/addons/addon-image/src/ImageAddon.ts index 0d040601..419fab20 100644 --- a/addons/addon-image/src/ImageAddon.ts +++ b/addons/addon-image/src/ImageAddon.ts @@ -51,7 +51,7 @@ const enum GaStatus { } -export class ImageAddon implements ITerminalAddon , IImageApi { +export class ImageAddon implements ITerminalAddon, IImageApi { private _opts: IImageAddonOptions; private _defaultOpts: IImageAddonOptions; private _storage: ImageStorage | undefined; @@ -93,7 +93,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi { // windowOptions.getCellSizePixels = true; // windowOptions.getWinSizeChars = true; // terminal.setOption('windowOptions', windowOptions); - const windowOps = terminal.options.windowOptions || {}; + const windowOps = terminal.options.windowOptions ?? {}; windowOps.getWinSizePixels = true; windowOps.getCellSizePixels = true; windowOps.getWinSizeChars = true; diff --git a/addons/addon-image/src/ImageRenderer.ts b/addons/addon-image/src/ImageRenderer.ts index e37169f3..1e87aa97 100644 --- a/addons/addon-image/src/ImageRenderer.ts +++ b/addons/addon-image/src/ImageRenderer.ts @@ -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, diff --git a/addons/addon-image/src/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts index f9b6eef6..aea9b5e5 100644 --- a/addons/addon-image/src/ImageStorage.ts +++ b/addons/addon-image/src/ImageStorage.ts @@ -132,8 +132,10 @@ export class ImageStorage implements IDisposable { ) { try { this.setLimit(this._opts.storageLimit); - } catch (e: any) { - console.error(e.message); + } catch (e: unknown) { + if (e instanceof Error) { + console.error(e.message); + } console.warn(`storageLimit is set to ${this.getLimit()} MB`); } this._viewportMetrics = { @@ -381,7 +383,7 @@ export class ImageStorage implements IDisposable { if (!line) return; for (let col = 0; col < cols; ++col) { if (line.getBg(col) & BgFlags.HAS_EXTENDED) { - let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS; + let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS; const imageId = e.imageId; if (imageId === undefined || imageId === -1) { continue; @@ -400,7 +402,7 @@ export class ImageStorage implements IDisposable { while ( ++col < cols && (line.getBg(col) & BgFlags.HAS_EXTENDED) - && (e = line._extendedAttrs[col] || EMPTY_ATTRS) + && (e = line._extendedAttrs[col] ?? EMPTY_ATTRS) && (e.imageId === imageId) && (e.tileId === startTile + count) ) { @@ -442,7 +444,7 @@ export class ImageStorage implements IDisposable { for (let row = 0; row < rows; ++row) { const line = buffer.lines.get(row) as IBufferLineExt; if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) { - const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS; + const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS; const imageId = e.imageId; if (imageId === undefined || imageId === -1) { continue; @@ -487,7 +489,7 @@ export class ImageStorage implements IDisposable { const buffer = this._terminal._core.buffer; const line = buffer.lines.get(y) as IBufferLineExt; if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { - const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS; + const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS; if (e.imageId && e.imageId !== -1) { const orig = this._images.get(e.imageId)?.orig; if (window.ImageBitmap && orig instanceof ImageBitmap) { @@ -507,7 +509,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) { diff --git a/addons/addon-image/test/playwright.config.ts b/addons/addon-image/test/playwright.config.ts index 22834be1..e2fe314e 100644 --- a/addons/addon-image/test/playwright.config.ts +++ b/addons/addon-image/test/playwright.config.ts @@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = { timeout: 10000, projects: [ { - name: 'ChromeStable', + name: 'Chromium', use: { - browserName: 'chromium', - channel: 'chrome' + browserName: 'chromium' } }, { diff --git a/addons/addon-ligatures/src/LigaturesAddon.ts b/addons/addon-ligatures/src/LigaturesAddon.ts index 2bde9cf6..5bba67e8 100644 --- a/addons/addon-ligatures/src/LigaturesAddon.ts +++ b/addons/addon-ligatures/src/LigaturesAddon.ts @@ -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) { // Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations - this._fallbackLigatures = (options?.fallbackLigatures || [ + this._fallbackLigatures = (options?.fallbackLigatures ?? [ '<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->', '<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=', '<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::', diff --git a/addons/addon-ligatures/src/font.ts b/addons/addon-ligatures/src/font.ts index 196651ac..60ab3d74 100644 --- a/addons/addon-ligatures/src/font.ts +++ b/addons/addon-ligatures/src/font.ts @@ -42,11 +42,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi if (status && status.state !== 'granted') { throw new Error('Permission to access local fonts not granted.'); } - } catch (err: any) { + } catch (err: unknown) { // A `TypeError` indicates the 'local-fonts' // permission is not yet implemented, so // only `throw` if this is _not_ the problem. - if (err.name !== 'TypeError') { + if (err instanceof Error && err.name !== 'TypeError') { throw err; } } @@ -60,8 +60,10 @@ export default async function load(fontFamily: string, cacheSize: number): Promi fonts[metadata.family].push(metadata); } fontsPromise = Promise.resolve(fonts); - } catch (err: any) { - console.error(err.name, err.message); + } catch (err: unknown) { + if (err instanceof Error) { + console.error(err.name, err.message); + } } } // Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603 @@ -76,13 +78,13 @@ export default async function load(fontFamily: string, cacheSize: number): Promi fonts[metadata.family].push(metadata); } fontsPromise = Promise.resolve(fonts); - } catch (err: any) { - console.error(err.name, err.message); + } catch (err: unknown) { + if (err instanceof Error) { + console.error(err.name, err.message); + } } } - if (!fontsPromise) { - fontsPromise = Promise.resolve({}); - } + fontsPromise ??= Promise.resolve({}); } const fonts = await fontsPromise; diff --git a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts index 3f06d672..e6259b6d 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts @@ -52,12 +52,10 @@ export function processLookaheadPosition( } processedEntries.add(currentEntry.entry); - if (!currentEntry.entry.forward) { - currentEntry.entry.forward = { - individual: {}, - range: [] - }; - } + currentEntry.entry.forward ??= { + individual: {}, + range: [] + }; // All glyphs at this position share ONE entry - lookahead just needs to match, // all paths lead to the same result @@ -97,12 +95,10 @@ export function processBacktrackPosition( } processedEntries.add(currentEntry.entry); - if (!currentEntry.entry.reverse) { - currentEntry.entry.reverse = { - individual: {}, - range: [] - }; - } + currentEntry.entry.reverse ??= { + individual: {}, + range: [] + }; // All glyphs at this position share ONE entry - backtrack just needs to match, // all paths lead to the same result diff --git a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts index 5eae2476..3a41e936 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts @@ -54,9 +54,6 @@ export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId return (glyphId + table.deltaGlyphId) % (2 ** 16); // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2 case 2: - // eslint-disable-next-line eqeqeq - return table.substitute[coverageIndex] != null - ? table.substitute[coverageIndex] - : null; + return table.substitute[coverageIndex] ?? null; } } diff --git a/addons/addon-ligatures/src/index.ts b/addons/addon-ligatures/src/index.ts index 0c67f510..fb8e85e7 100644 --- a/addons/addon-ligatures/src/index.ts +++ b/addons/addon-ligatures/src/index.ts @@ -30,7 +30,7 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = [] let currentFontName: string | undefined = undefined; let font: Font | undefined = undefined; let loadingState: LoadingState = LoadingState.UNLOADED; - let loadError: any | undefined = undefined; + let loadError: unknown = undefined; return term.registerCharacterJoiner((text: string): [number, number][] => { // If the font hasn't been loaded yet, load it and return an empty result diff --git a/addons/addon-progress/test/playwright.config.ts b/addons/addon-progress/test/playwright.config.ts index 22834be1..e2fe314e 100644 --- a/addons/addon-progress/test/playwright.config.ts +++ b/addons/addon-progress/test/playwright.config.ts @@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = { timeout: 10000, projects: [ { - name: 'ChromeStable', + name: 'Chromium', use: { - browserName: 'chromium', - channel: 'chrome' + browserName: 'chromium' } }, { diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index ffbe9401..8e647a9e 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -4,7 +4,7 @@ */ import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm'; -import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search'; +import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { disposableTimeout } from 'vs/base/common/async'; @@ -222,7 +222,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp * @param result The result to select. * @returns Whether a result was selected. */ - private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean { + private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean { if (!this._terminal || !this._decorationManager) { return false; } diff --git a/addons/addon-search/src/SearchEngine.ts b/addons/addon-search/src/SearchEngine.ts index a78be61e..b9991974 100644 --- a/addons/addon-search/src/SearchEngine.ts +++ b/addons/addon-search/src/SearchEngine.ts @@ -198,9 +198,7 @@ export class SearchEngine { } } - if (!result) { - result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); - } + result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch); // Search from startRow - 1 to top if (!result) { diff --git a/addons/addon-search/test/playwright.config.ts b/addons/addon-search/test/playwright.config.ts index 22834be1..e2fe314e 100644 --- a/addons/addon-search/test/playwright.config.ts +++ b/addons/addon-search/test/playwright.config.ts @@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = { timeout: 10000, projects: [ { - name: 'ChromeStable', + name: 'Chromium', use: { - browserName: 'chromium', - channel: 'chrome' + browserName: 'chromium' } }, { diff --git a/addons/addon-serialize/src/SerializeAddon.test.ts b/addons/addon-serialize/src/SerializeAddon.test.ts index e5307e1a..6e8c2ad4 100644 --- a/addons/addon-serialize/src/SerializeAddon.test.ts +++ b/addons/addon-serialize/src/SerializeAddon.test.ts @@ -228,7 +228,7 @@ describe('SerializeAddon', () => { it('empty terminal with selection turned off', () => { const output = serializeAddon.serializeAsHTML(); assert.notEqual(output, ''); - assert.equal((output.match(/
{10}<\/span><\/div>/g) || []).length, 2); + assert.equal((output.match(/
{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(/
terminal<\/span><\/div>/g) || []).length, 1, output); + assert.equal((output.match(/
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(/
<a>&pi;<\/span><\/div>/g) || []).length, 1, output); + assert.equal((output.match(/
<a>&pi;<\/span><\/div>/g) ?? []).length, 1, output); }); it('serializes rows within a provided range', async () => { @@ -267,7 +267,7 @@ describe('SerializeAddon', () => { startCol: 4 } }); - const rowMatches = output.match(/
.*?<\/span><\/div>/g) || []; + const rowMatches = output.match(/
.*?<\/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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>/g) || []).length, 1, output); - assert.equal((output.match(/termi<\/span>/g) || []).length, 1, output); - assert.equal((output.match(/nal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/ <\/span>/g) ?? []).length, 1, output); + assert.equal((output.match(/termi<\/span>/g) ?? []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/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(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); }); }); diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index c9c66ce8..fadeb5bb 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -457,7 +457,7 @@ class StringSerializeHandler extends BaseSerializeHandler { } } -export class SerializeAddon implements ITerminalAddon , ISerializeApi { +export class SerializeAddon implements ITerminalAddon, ISerializeApi { private _terminal: Terminal | undefined; public activate(terminal: Terminal): void { @@ -601,7 +601,7 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { throw new Error('Cannot use addon until it has been loaded'); } - return this._serializeBufferAsHTML(this._terminal, options || {}); + return this._serializeBufferAsHTML(this._terminal, options ?? {}); } public dispose(): void { } @@ -630,20 +630,6 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { } } - private _padStart(target: string, targetLength: number, padString: string): string { - targetLength = targetLength >> 0; - padString = padString ?? ' '; - if (target.length > targetLength) { - return target; - } - - targetLength -= target.length; - if (targetLength > padString.length) { - padString += padString.repeat(targetLength / padString.length); - } - return padString.slice(0, targetLength) + target; - } - protected _beforeSerialize(rows: number, start: number, end: number): void { this._htmlContent += '
';
 
@@ -680,7 +666,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
         (color >>  8) & 255,
         (color      ) & 255
       ];
-      return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
+      return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
     }
     if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
       return this._ansiColors[color].css;
@@ -700,7 +686,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
         (color >>  8) & 255,
         (color      ) & 255
       ];
-      return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
+      return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
     }
     // Palette color
     return this._ansiColors[color].css;
diff --git a/addons/addon-serialize/test/playwright.config.ts b/addons/addon-serialize/test/playwright.config.ts
index 22834be1..e2fe314e 100644
--- a/addons/addon-serialize/test/playwright.config.ts
+++ b/addons/addon-serialize/test/playwright.config.ts
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
   timeout: 10000,
   projects: [
     {
-      name: 'ChromeStable',
+      name: 'Chromium',
       use: {
-        browserName: 'chromium',
-        channel: 'chrome'
+        browserName: 'chromium'
       }
     },
     {
diff --git a/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts b/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts
index 6e4d2266..574a36cc 100644
--- a/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts
+++ b/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts
@@ -9,19 +9,15 @@ import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
 import type { UnicodeGraphemesAddon as IUnicodeGraphemesApi } from '@xterm/addon-unicode-graphemes';
 import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider';
 
-export class UnicodeGraphemesAddon implements ITerminalAddon , IUnicodeGraphemesApi {
+export class UnicodeGraphemesAddon implements ITerminalAddon, IUnicodeGraphemesApi {
   private _provider15Graphemes?: UnicodeGraphemeProvider;
   private _provider15?: UnicodeGraphemeProvider;
   private _unicode?: IUnicodeHandling;
   private _oldVersion: string = '';
 
   public activate(terminal: Terminal): void {
-    if (! this._provider15) {
-      this._provider15 = new UnicodeGraphemeProvider(false);
-    }
-    if (! this._provider15Graphemes) {
-      this._provider15Graphemes = new UnicodeGraphemeProvider(true);
-    }
+    this._provider15 ??= new UnicodeGraphemeProvider(false);
+    this._provider15Graphemes ??= new UnicodeGraphemeProvider(true);
     const unicode = terminal.unicode;
     this._unicode = unicode;
     unicode.register(this._provider15);
diff --git a/addons/addon-unicode-graphemes/src/third-party/UnicodeProperties.ts b/addons/addon-unicode-graphemes/src/third-party/UnicodeProperties.ts
index 0ee147f8..e0af5ce8 100644
--- a/addons/addon-unicode-graphemes/src/third-party/UnicodeProperties.ts
+++ b/addons/addon-unicode-graphemes/src/third-party/UnicodeProperties.ts
@@ -1,7 +1,7 @@
 import UnicodeTrie from './unicode-trie';
 const trieRaw = "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";
 
-declare const Buffer: any;
+declare const Buffer: { from(s: string, encoding: string): Uint8Array } | undefined;
 function _dec(s: string): Uint8Array {
     if (typeof Buffer !== 'undefined') return Buffer.from(s, 'base64');
     const bs = atob(s);
diff --git a/addons/addon-unicode-graphemes/test/playwright.config.ts b/addons/addon-unicode-graphemes/test/playwright.config.ts
index 22834be1..e2fe314e 100644
--- a/addons/addon-unicode-graphemes/test/playwright.config.ts
+++ b/addons/addon-unicode-graphemes/test/playwright.config.ts
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
   timeout: 10000,
   projects: [
     {
-      name: 'ChromeStable',
+      name: 'Chromium',
       use: {
-        browserName: 'chromium',
-        channel: 'chrome'
+        browserName: 'chromium'
       }
     },
     {
diff --git a/addons/addon-unicode11/src/Unicode11Addon.ts b/addons/addon-unicode11/src/Unicode11Addon.ts
index 301145ee..897f4f40 100644
--- a/addons/addon-unicode11/src/Unicode11Addon.ts
+++ b/addons/addon-unicode11/src/Unicode11Addon.ts
@@ -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());
   }
diff --git a/addons/addon-unicode11/test/playwright.config.ts b/addons/addon-unicode11/test/playwright.config.ts
index 22834be1..e2fe314e 100644
--- a/addons/addon-unicode11/test/playwright.config.ts
+++ b/addons/addon-unicode11/test/playwright.config.ts
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
   timeout: 10000,
   projects: [
     {
-      name: 'ChromeStable',
+      name: 'Chromium',
       use: {
-        browserName: 'chromium',
-        channel: 'chrome'
+        browserName: 'chromium'
       }
     },
     {
diff --git a/addons/addon-web-fonts/test/playwright.config.ts b/addons/addon-web-fonts/test/playwright.config.ts
index 22834be1..e2fe314e 100644
--- a/addons/addon-web-fonts/test/playwright.config.ts
+++ b/addons/addon-web-fonts/test/playwright.config.ts
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
   timeout: 10000,
   projects: [
     {
-      name: 'ChromeStable',
+      name: 'Chromium',
       use: {
-        browserName: 'chromium',
-        channel: 'chrome'
+        browserName: 'chromium'
       }
     },
     {
diff --git a/addons/addon-web-links/src/WebLinksAddon.ts b/addons/addon-web-links/src/WebLinksAddon.ts
index b3f0548c..3328a3c3 100644
--- a/addons/addon-web-links/src/WebLinksAddon.ts
+++ b/addons/addon-web-links/src/WebLinksAddon.ts
@@ -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));
   }
 
diff --git a/addons/addon-web-links/test/playwright.config.ts b/addons/addon-web-links/test/playwright.config.ts
index 22834be1..e2fe314e 100644
--- a/addons/addon-web-links/test/playwright.config.ts
+++ b/addons/addon-web-links/test/playwright.config.ts
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
   timeout: 10000,
   projects: [
     {
-      name: 'ChromeStable',
+      name: 'Chromium',
       use: {
-        browserName: 'chromium',
-        channel: 'chrome'
+        browserName: 'chromium'
       }
     },
     {
diff --git a/addons/addon-webgl/src/DevicePixelObserver.ts b/addons/addon-webgl/src/DevicePixelObserver.ts
index 33d733ef..dcd8de0a 100644
--- a/addons/addon-webgl/src/DevicePixelObserver.ts
+++ b/addons/addon-webgl/src/DevicePixelObserver.ts
@@ -3,7 +3,7 @@
  * @license MIT
  */
 
-import { toDisposable, IDisposable } from 'vs/base/common/lifecycle';
+import { toDisposable, IDisposable } from 'common/Lifecycle';
 
 export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable {
   // Observe any resizes to the element and extract the actual pixel size of the element if the
diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts
index 6b359d4b..f5be446f 100644
--- a/addons/addon-webgl/src/GlyphRenderer.ts
+++ b/addons/addon-webgl/src/GlyphRenderer.ts
@@ -5,7 +5,7 @@
 import { TextureAtlas } from './TextureAtlas';
 import { IRenderDimensions } from 'browser/renderer/shared/Types';
 import { NULL_CELL_CODE } from 'common/buffer/Constants';
-import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
 import { Terminal } from '@xterm/xterm';
 import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types';
 import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
diff --git a/addons/addon-webgl/src/RectangleRenderer.ts b/addons/addon-webgl/src/RectangleRenderer.ts
index 8ace9076..d22068ba 100644
--- a/addons/addon-webgl/src/RectangleRenderer.ts
+++ b/addons/addon-webgl/src/RectangleRenderer.ts
@@ -7,7 +7,7 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types';
 import { IThemeService } from 'browser/services/Services';
 import { ReadonlyColorSet } from 'browser/Types';
 import { Attributes, FgFlags } from 'common/buffer/Constants';
-import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
 import { IColor } from 'common/Types';
 import { Terminal } from '@xterm/xterm';
 import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts
index 71624fe8..aa0e2653 100644
--- a/addons/addon-webgl/src/TextureAtlas.ts
+++ b/addons/addon-webgl/src/TextureAtlas.ts
@@ -15,7 +15,7 @@ import { IColor } from 'common/Types';
 import { AttributeData } from 'common/buffer/AttributeData';
 import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
 import { IUnicodeService } from 'common/services/Services';
-import { Emitter } from 'vs/base/common/event';
+import { Emitter } from 'common/Event';
 
 /**
  * A shared object which is used to draw nothing for a particular cell.
@@ -410,7 +410,7 @@ export class TextureAtlas implements ITextureAtlas {
     const cache = this._getContrastCache(dim);
     const adjustedColor = cache.getColor(bg, fg);
     if (adjustedColor !== undefined) {
-      return adjustedColor || undefined;
+      return adjustedColor ?? undefined;
     }
 
     const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
diff --git a/addons/addon-webgl/src/Types.ts b/addons/addon-webgl/src/Types.ts
index e030e579..67c9408d 100644
--- a/addons/addon-webgl/src/Types.ts
+++ b/addons/addon-webgl/src/Types.ts
@@ -7,7 +7,7 @@ import { FontWeight } from '@xterm/xterm';
 import { IColorSet } from 'browser/Types';
 import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
 import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types';
-import type { Event } from 'vs/base/common/event';
+import type { IEvent } from 'common/Event';
 
 export interface IRenderModel {
   cells: Uint32Array;
@@ -58,8 +58,8 @@ export interface ICharAtlasConfig {
 export interface ITextureAtlas extends IDisposable {
   readonly pages: { canvas: HTMLCanvasElement, version: number }[];
 
-  onAddTextureAtlasCanvas: Event;
-  onRemoveTextureAtlasCanvas: Event;
+  onAddTextureAtlasCanvas: IEvent;
+  onRemoveTextureAtlasCanvas: IEvent;
 
   /**
    * Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
diff --git a/addons/addon-webgl/src/WebglAddon.ts b/addons/addon-webgl/src/WebglAddon.ts
index 72e0835e..f21fb704 100644
--- a/addons/addon-webgl/src/WebglAddon.ts
+++ b/addons/addon-webgl/src/WebglAddon.ts
@@ -7,15 +7,14 @@ import type { ITerminalAddon, Terminal } from '@xterm/xterm';
 import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl';
 import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
 import { ITerminal } from 'browser/Types';
-import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
 import { getSafariVersion, isSafari } from 'common/Platform';
-import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
+import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
 import { IWebGL2RenderingContext } from './Types';
 import { WebglRenderer } from './WebglRenderer';
-import { setTraceLogger } from 'common/services/LogService';
-import { Emitter, Event } from 'vs/base/common/event';
+import { Emitter, EventUtils } from 'common/Event';
 
-export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi {
+export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi {
   private _terminal?: Terminal;
   private _renderer?: WebglRenderer;
 
@@ -66,13 +65,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
     const charSizeService: ICharSizeService = unsafeCore._charSizeService;
     const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
     const decorationService: IDecorationService = unsafeCore._decorationService;
-    const logService: ILogService = unsafeCore._logService;
     const themeService: IThemeService = unsafeCore._themeService;
 
-    // Set trace logger just in case it hasn't been yet which could happen when the addon is
-    // bundled separately to the core module
-    setTraceLogger(logService);
-
     this._renderer = this._register(new WebglRenderer(
       terminal,
       characterJoinerService,
@@ -85,10 +79,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
       this._customGlyphs,
       this._preserveDrawingBuffer
     ));
-    this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss));
-    this._register(Event.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
-    this._register(Event.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
-    this._register(Event.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
+    this._register(EventUtils.forward(this._renderer.onContextLoss, this._onContextLoss));
+    this._register(EventUtils.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
+    this._register(EventUtils.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
+    this._register(EventUtils.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
     renderService.setRenderer(this._renderer);
 
     this._register(toDisposable(() => {
diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts
index 345437d5..05af6527 100644
--- a/addons/addon-webgl/src/WebglRenderer.ts
+++ b/addons/addon-webgl/src/WebglRenderer.ts
@@ -22,9 +22,9 @@ import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET
 import { IWebGL2RenderingContext, type ITextureAtlas } from './Types';
 import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
 import { IRenderLayer } from './renderLayer/Types';
-import { Emitter, Event } from 'vs/base/common/event';
+import { Emitter, EventUtils } from 'common/Event';
 import { addDisposableListener } from 'vs/base/browser/dom';
-import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
+import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
 import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
 
 export class WebglRenderer extends Disposable implements IRenderer {
@@ -290,8 +290,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
     if (this._charAtlas !== atlas) {
       this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
       this._charAtlasDisposable.value = combinedDisposable(
-        Event.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
-        Event.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
+        EventUtils.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
+        EventUtils.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
       );
     }
     this._charAtlas = atlas;
diff --git a/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts
index 85b170e7..5b116153 100644
--- a/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts
+++ b/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts
@@ -7,7 +7,7 @@ import { ReadonlyColorSet } from 'browser/Types';
 import { acquireTextureAtlas } from '../CharAtlasCache';
 import { IRenderDimensions } from 'browser/renderer/shared/Types';
 import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
-import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
 import { CellData } from 'common/buffer/CellData';
 import { IOptionsService } from 'common/services/Services';
 import { Terminal } from '@xterm/xterm';
diff --git a/addons/addon-webgl/test/playwright.config.ts b/addons/addon-webgl/test/playwright.config.ts
index 22834be1..e2fe314e 100644
--- a/addons/addon-webgl/test/playwright.config.ts
+++ b/addons/addon-webgl/test/playwright.config.ts
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
   timeout: 10000,
   projects: [
     {
-      name: 'ChromeStable',
+      name: 'Chromium',
       use: {
-        browserName: 'chromium',
-        channel: 'chrome'
+        browserName: 'chromium'
       }
     },
     {
diff --git a/demo/client/client.ts b/demo/client/client.ts
index baa989c1..c16216de 100644
--- a/demo/client/client.ts
+++ b/demo/client/client.ts
@@ -16,9 +16,12 @@ if ('WebAssembly' in window) {
 import { Terminal, ITerminalOptions, type ITheme } from '@xterm/xterm';
 import { AttachAddon } from '@xterm/addon-attach';
 import { AddonImageWindow } from './components/window/addonImageWindow';
+import { AddonLigaturesWindow } from './components/window/addonLigaturesWindow';
+import { AddonProgressWindow } from './components/window/addonProgressWindow';
 import { AddonSearchWindow } from './components/window/addonSearchWindow';
 import { AddonSerializeWindow } from './components/window/addonSerializeWindow';
 import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow';
+import { AddonWebLinksWindow } from './components/window/addonWebLinksWindow';
 import { AddonsWindow } from './components/window/addonsWindow';
 import { CellInspectorWindow } from './components/window/cellInspectorWindow';
 import { ControlBar } from './components/controlBar';
@@ -58,11 +61,11 @@ export interface IWindowWithTerminal extends Window {
 }
 declare let window: IWindowWithTerminal;
 
-let term;
-let protocol;
-let socketURL;
-let socket;
-let pid;
+let term: Terminal | null;
+let protocol: string;
+let socketURL: string;
+let socket: WebSocket | null;
+let pid: string;
 let controlBar: ControlBar;
 let addonsWindow: AddonsWindow;
 let addonSearchWindow: AddonSearchWindow;
@@ -73,7 +76,7 @@ const addons: AddonCollection = {
   attach: { name: 'attach', ctor: AttachAddon, canChange: false },
   clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true },
   fit: { name: 'fit', ctor: FitAddon, canChange: false },
-  image: { name: 'image', ctor: ImageAddon, canChange: true },
+  image: { name: 'image', ctor: ImageAddon!, canChange: true },
   progress: { name: 'progress', ctor: ProgressAddon, canChange: true },
   search: { name: 'search', ctor: SearchAddon, canChange: true },
   serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
@@ -116,8 +119,8 @@ const xtermjsTheme = {
   brightWhite: '#FFFFFF'
 } satisfies ITheme;
 function setPadding(): void {
-  term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
-  addons.fit.instance.fit();
+  term!.element!.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
+  addons.fit.instance!.fit();
 }
 
 function getSearchOptions(): ISearchOptions {
@@ -141,7 +144,7 @@ const disposeRecreateButtonHandler: () => void = () => {
   if (term) {
     term.dispose();
     term = null;
-    window.term = null;
+    (window as any).term = null;
     socket = null;
     addons.attach.instance = undefined;
     addons.clipboard.instance = undefined;
@@ -154,10 +157,10 @@ const disposeRecreateButtonHandler: () => void = () => {
     addons.ligatures.instance = undefined;
     addons.webLinks.instance = undefined;
     addons.webgl.instance = undefined;
-    document.getElementById('dispose').innerHTML = 'Recreate Terminal';
+    document.getElementById('dispose')!.innerHTML = 'Recreate Terminal';
   } else {
     createTerminal();
-    document.getElementById('dispose').innerHTML = 'Dispose terminal';
+    document.getElementById('dispose')!.innerHTML = 'Dispose terminal';
   }
 };
 
@@ -165,7 +168,7 @@ const createNewWindowButtonHandler: () => void = () => {
   if (term) {
     disposeRecreateButtonHandler();
   }
-  const win = window.open();
+  const win = window.open()!;
   terminalContainer = win.document.createElement('div');
   terminalContainer.id = 'terminal-container';
   win.document.body.appendChild(terminalContainer);
@@ -207,17 +210,20 @@ if (document.location.pathname === '/test') {
 } else {
   const typedTerm = createTerminal();
 
-  controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), []);
+  controlBar = new ControlBar(document.getElementById('sidebar')!, document.querySelector('.banner-tabs')!, []);
   optionsWindow = controlBar.registerWindow(new OptionsWindow(typedTerm, addons, { updateTerminalSize, updateTerminalContainerBackground }));
   const styleWindow = controlBar.registerWindow(new StyleWindow(typedTerm, addons));
   controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons));
   controlBar.registerWindow(new VtWindow(typedTerm, addons));
   addonsWindow = controlBar.registerWindow(new AddonsWindow(typedTerm, addons));
-  addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonLigaturesWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonProgressWindow(typedTerm, addons), { afterId: 'addon-ligatures', hidden: true, italics: true });
+  addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addon-progress', hidden: true, italics: true });
   controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true });
-  controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
-  controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
-  addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonWebLinksWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
+  addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-links', hidden: true, italics: true });
   controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
   actionElements = {
     findNext: addonSearchWindow.findNextInput,
@@ -229,48 +235,51 @@ if (document.location.pathname === '/test') {
   // TODO: Most of below should be encapsulated within windows
   paddingElement = styleWindow.paddingElement;
 
-  controlBar.setTabVisible('addon-webgl', true);
+  controlBar.setTabVisible('addon-image', !!addons.image.instance);
+  controlBar.setTabVisible('addon-ligatures', !!addons.ligatures.instance);
+  controlBar.setTabVisible('addon-progress', !!addons.progress.instance);
   controlBar.setTabVisible('addon-search', true);
   controlBar.setTabVisible('addon-serialize', true);
-  controlBar.setTabVisible('addon-image', true);
   controlBar.setTabVisible('addon-web-fonts', true);
-  addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
-  addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
-  addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
-  addons.webgl.instance.onRemoveTextureAtlasCanvas(e => addonWebglWindow.removeTextureAtlas(e));
+  controlBar.setTabVisible('addon-web-links', !!addons.webLinks.instance);
+  controlBar.setTabVisible('addon-webgl', true);
+  addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
+  addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
+  addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
+  addons.webgl.instance!.onRemoveTextureAtlasCanvas(e => addonWebglWindow.removeTextureAtlas(e));
 
   paddingElement.value = '0';
   addDomListener(paddingElement, 'change', setPadding);
   addDomListener(actionElements.findNext, 'keydown', (e) => {
     if (e.key === 'Enter') {
-      addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
+      addons.search.instance!.findNext(actionElements.findNext.value, getSearchOptions());
       e.preventDefault();
     }
   });
   addDomListener(actionElements.findNext, 'input', (e) => {
-    addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
+    addons.search.instance!.findNext(actionElements.findNext.value, getSearchOptions());
   });
   addDomListener(actionElements.findPrevious, 'keydown', (e) => {
     if (e.key === 'Enter') {
-      addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
+      addons.search.instance!.findPrevious(actionElements.findPrevious.value, getSearchOptions());
       e.preventDefault();
     }
   });
   addDomListener(actionElements.findPrevious, 'input', (e) => {
-    addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
+    addons.search.instance!.findPrevious(actionElements.findPrevious.value, getSearchOptions());
   });
   addDomListener(actionElements.findNext, 'blur', (e) => {
-    addons.search.instance.clearActiveDecoration();
+    addons.search.instance!.clearActiveDecoration();
   });
   addDomListener(actionElements.findPrevious, 'blur', (e) => {
-    addons.search.instance.clearActiveDecoration();
+    addons.search.instance!.clearActiveDecoration();
   });
 }
 
 function createTerminal(): Terminal {
   // Clean terminal
-  while (terminalContainer.children.length) {
-    terminalContainer.removeChild(terminalContainer.children[0]);
+  while (terminalContainer!.children.length) {
+    terminalContainer!.removeChild(terminalContainer!.children[0]);
   }
 
   const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
@@ -282,10 +291,7 @@ function createTerminal(): Terminal {
       buildNumber: 22621
     } : undefined,
     fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"',
-    theme: { ...xtermjsTheme },
-    vtExtensions: {
-      win32InputMode: isWindows
-    }
+    theme: { ...xtermjsTheme }
   } as ITerminalOptions);
 
   // Load addons
@@ -293,7 +299,7 @@ function createTerminal(): Terminal {
   addons.search.instance = new SearchAddon();
   addons.serialize.instance = new SerializeAddon();
   addons.fit.instance = new FitAddon();
-  addons.image.instance = new ImageAddon();
+  addons.image.instance = new ImageAddon!();
   addons.progress.instance = new ProgressAddon();
   addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon();
   addons.clipboard.instance = new ClipboardAddon();
@@ -314,7 +320,7 @@ function createTerminal(): Terminal {
   typedTerm.loadAddon(addons.webFonts.instance);
   typedTerm.loadAddon(addons.clipboard.instance);
 
-  window.term = term;  // Expose `term` to window for debugging purposes
+  (window as any).term = term;  // Expose `term` to window for debugging purposes
   term.onResize((size: { cols: number, rows: number }) => {
     if (!pid) {
       return;
@@ -335,7 +341,7 @@ function createTerminal(): Terminal {
   if (addons.webgl.instance) {
     try {
       typedTerm.loadAddon(addons.webgl.instance);
-      term.open(terminalContainer);
+      term.open(terminalContainer!);
     } catch (e) {
       console.warn('error during loading webgl addon:', e);
       addons.webgl.instance.dispose();
@@ -344,7 +350,7 @@ function createTerminal(): Terminal {
   }
   if (!typedTerm.element) {
     // webgl loading failed for some reason, attach with DOM renderer
-    term.open(terminalContainer);
+    term.open(terminalContainer!);
   }
 
   term.focus();
@@ -354,13 +360,13 @@ function createTerminal(): Terminal {
     if (optionsWindow.autoResize) {
       // In general this should be debounced to avoid excessive work on the main
       // thread by firing the expensive resize action repeatedly
-      addons.fit.instance.fit();
+      addons.fit.instance!.fit();
     }
   });
-  resizeObserver.observe(terminalContainer);
+  resizeObserver.observe(terminalContainer!);
 
   window.addEventListener('resize', () => {
-    terminalContainer.style.width = document.body.clientWidth + 'px';
+    terminalContainer!.style.width = document.body.clientWidth + 'px';
   });
 
   // fit is called within a setTimeout, cols and rows need this.
@@ -376,7 +382,7 @@ function createTerminal(): Terminal {
     } 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 + '&pixelWidth=' + pixelWidth + '&pixelHeight=' + pixelHeight, { method: 'POST' });
       const processId = await res.text();
       pid = processId;
       socketURL += processId;
@@ -391,52 +397,52 @@ function createTerminal(): Terminal {
 }
 
 function runRealTerminal(): void {
-  addons.attach.instance = new AttachAddon(socket);
-  term.loadAddon(addons.attach.instance);
-  term._initialized = true;
-  initAddons(term);
+  addons.attach.instance = new AttachAddon(socket!);
+  term!.loadAddon(addons.attach.instance);
+  (term as any)._initialized = true;
+  initAddons(term!);
 }
 
 function runFakeTerminal(): void {
-  if (term._initialized) {
+  if ((term as any)._initialized) {
     return;
   }
 
-  term._initialized = true;
-  initAddons(term);
+  (term as any)._initialized = true;
+  initAddons(term!);
 
-  term.prompt = () => {
-    term.write('\r\n$ ');
+  (term as any).prompt = () => {
+    term!.write('\r\n$ ');
   };
 
-  term.writeln('Welcome to xterm.js');
-  term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
-  term.writeln('Type some keys and commands to play around.');
-  term.writeln('');
-  term.prompt();
+  term!.writeln('Welcome to xterm.js');
+  term!.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
+  term!.writeln('Type some keys and commands to play around.');
+  term!.writeln('');
+  (term as any).prompt();
 
-  term.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
+  term!.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
     const ev = e.domEvent;
     const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
 
     if (ev.keyCode === 13) {
-      term.prompt();
+      (term as any).prompt();
     } else if (ev.keyCode === 8) {
       // Do not delete the prompt
-      if (term._core.buffer.x > 2) {
-        term.write('\b \b');
+      if ((term as any)._core.buffer.x > 2) {
+        term!.write('\b \b');
       }
     } else if (printable) {
-      term.write(e.key);
+      term!.write(e.key);
     }
   });
 }
 
 function updateTerminalContainerBackground(): void {
-  if (term.options.allowTransparency) {
-    terminalContainer.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px';
+  if (term!.options.allowTransparency) {
+    terminalContainer!.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px';
   } else {
-    terminalContainer.style.background = term.options.theme?.background ?? '#000000';
+    terminalContainer!.style.background = term!.options.theme?.background ?? '#000000';
   }
 }
 
@@ -446,19 +452,19 @@ function initAddons(term: Terminal): void {
   function postInitWebgl(): void {
     controlBar.setTabVisible('addon-webgl', true);
     setTimeout(() => {
-      addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
-      addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
-      addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
+      addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
+      addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
+      addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
     }, 500);
   }
   function preDisposeWebgl(): void {
     controlBar.setTabVisible('addon-webgl', false);
-    if (addons.webgl.instance.textureAtlas) {
-      addons.webgl.instance.textureAtlas.remove();
+    if (addons.webgl.instance!.textureAtlas) {
+      addons.webgl.instance!.textureAtlas.remove();
     }
   }
 
-  Object.keys(addons).forEach((name: AddonType) => {
+  (Object.keys(addons) as AddonType[]).forEach(name => {
     const addon = addons[name];
     const checkbox = document.createElement('input') as HTMLInputElement;
     checkbox.type = 'checkbox';
@@ -473,12 +479,12 @@ function initAddons(term: Terminal): void {
       term.unicode.activeVersion = '15-graphemes';
     }
     if (name === 'search' && checkbox.checked) {
-      addons[name].instance.onDidChangeResults(e => updateFindResults(e));
+      addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
     }
     addDomListener(checkbox, 'change', () => {
       if (name === 'image') {
         if (checkbox.checked) {
-          const ctorOptionsJson = document.querySelector('#image-options').value;
+          const ctorOptionsJson = document.querySelector('#image-options')!.value;
           addon.instance = ctorOptionsJson
             ? new addons[name].ctor(JSON.parse(ctorOptionsJson))
             : new addons[name].ctor();
@@ -504,9 +510,15 @@ function initAddons(term: Terminal): void {
             term.unicode.activeVersion = '15-graphemes';
           } else if (name === 'search') {
             controlBar.setTabVisible('addon-search', true);
-            addons[name].instance.onDidChangeResults(e => updateFindResults(e));
+            addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
           } else if (name === 'serialize') {
             controlBar.setTabVisible('addon-serialize', true);
+          } else if (name === 'ligatures') {
+            controlBar.setTabVisible('addon-ligatures', true);
+          } else if (name === 'progress') {
+            controlBar.setTabVisible('addon-progress', true);
+          } else if (name === 'webLinks') {
+            controlBar.setTabVisible('addon-web-links', true);
           }
         }
         catch {
@@ -523,6 +535,12 @@ function initAddons(term: Terminal): void {
           controlBar.setTabVisible('addon-search', false);
         } else if (name === 'serialize') {
           controlBar.setTabVisible('addon-serialize', false);
+        } else if (name === 'ligatures') {
+          controlBar.setTabVisible('addon-ligatures', false);
+        } else if (name === 'progress') {
+          controlBar.setTabVisible('addon-progress', false);
+        } else if (name === 'webLinks') {
+          controlBar.setTabVisible('addon-web-links', false);
         }
         addon.instance!.dispose();
         addon.instance = undefined;
@@ -594,17 +612,17 @@ function updateFindResults(e: { resultIndex: number, resultCount: number } | und
 
 function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
   element.addEventListener(type, handler);
-  term._core._register({ dispose: () => element.removeEventListener(type, handler) });
+  (term as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
 }
 
 function updateTerminalSize(): void {
   const width = optionsWindow.autoResize ? '100%'
-    : (term.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px';
+    : ((term as any).dimensions.css.canvas.width + (term as any)._core.viewport.scrollBarWidth).toString() + 'px';
   const height = optionsWindow.autoResize ? '100%'
-    : (term.dimensions.css.canvas.height).toString() + 'px';
-  terminalContainer.style.width = width;
-  terminalContainer.style.height = height;
-  addons.fit.instance.fit();
+    : ((term as any).dimensions.css.canvas.height).toString() + 'px';
+  terminalContainer!.style.width = width;
+  terminalContainer!.style.height = height;
+  addons.fit.instance!.fit();
 }
 
 (console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
diff --git a/demo/client/components/window/addonImageWindow.ts b/demo/client/components/window/addonImageWindow.ts
index 6c814622..ce5ab9de 100644
--- a/demo/client/components/window/addonImageWindow.ts
+++ b/demo/client/components/window/addonImageWindow.ts
@@ -5,14 +5,15 @@
 
 import { BaseWindow } from './baseWindow';
 import type { IControlWindow } from '../controlBar';
+import type { IImageAddonOptions } from '@xterm/addon-image';
 
 export class AddonImageWindow extends BaseWindow implements IControlWindow {
   public readonly id = 'addon-image';
   public readonly label = 'image';
 
-  private _imageStorageLimitInput: HTMLInputElement;
-  private _imageShowPlaceholderCheckbox: HTMLInputElement;
-  private _imageOptionsTextarea: HTMLTextAreaElement;
+  private _imageStorageLimitInput!: HTMLInputElement;
+  private _imageShowPlaceholderCheckbox!: HTMLInputElement;
+  private _imageOptionsTextarea!: HTMLTextAreaElement;
 
   public build(container: HTMLElement): void {
     // Storage limit
@@ -46,6 +47,20 @@ 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'));
+
+    const dl = document.createElement('dl');
+    const dt = document.createElement('dt');
+    dt.textContent = 'Image Test';
+    dl.appendChild(dt);
+    this._addDdWithButton(dl, 'image-demo1', 'snake (sixel)');
+    this._addDdWithButton(dl, 'image-demo2', 'oranges (sixel)');
+    this._addDdWithButton(dl, 'image-demo3', 'palette (iip)');
+    container.appendChild(dl);
+
+    this._initImageAddonExposed();
   }
 
   public get imageStorageLimitInput(): HTMLInputElement {
@@ -59,4 +74,93 @@ 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('#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('#image-showplaceholder')!;
+    showPlaceholderElement.checked = imageAddon.showPlaceholder;
+    this._addDomListener(showPlaceholderElement, 'change', () => {
+      imageAddon.showPlaceholder = showPlaceholderElement.checked;
+    });
+    const ctorOptionsElement = document.querySelector('#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\\`);
+      });
+
+    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
+    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) });
+  }
 }
diff --git a/demo/client/components/window/addonLigaturesWindow.ts b/demo/client/components/window/addonLigaturesWindow.ts
new file mode 100644
index 00000000..273ec04e
--- /dev/null
+++ b/demo/client/components/window/addonLigaturesWindow.ts
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2026 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { BaseWindow } from './baseWindow';
+import type { IControlWindow } from '../controlBar';
+
+export class AddonLigaturesWindow extends BaseWindow implements IControlWindow {
+  public readonly id = 'addon-ligatures';
+  public readonly label = 'ligatures';
+
+  public build(container: HTMLElement): void {
+    const dl = document.createElement('dl');
+    const dt = document.createElement('dt');
+    dt.textContent = 'Ligatures Addon';
+    dl.appendChild(dt);
+
+    const dd = document.createElement('dd');
+    const button = document.createElement('button');
+    button.id = 'ligatures-test';
+    button.textContent = 'Common ligatures';
+    button.title = 'Write common ligatures sequences';
+    button.addEventListener('click', () => this._ligaturesTest());
+    dd.appendChild(button);
+    dl.appendChild(dd);
+
+    container.appendChild(dl);
+  }
+
+  private _ligaturesTest(): void {
+    this._terminal.write([
+      '',
+      '-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
+      '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
+      '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
+      '<~~  /> ~~> == != /= ~= <> === !== !=== =/= =!=',
+      '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
+      '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -|  ---> ->- >- >>-',
-    '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
-    '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
-    '<~~  /> ~~> == != /= ~= <> === !== !=== =/= =!=',
-    '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
-    '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -|