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/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/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/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts index 059849ea..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 = { 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/font.ts b/addons/addon-ligatures/src/font.ts index 1316932a..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,8 +78,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); + } } } fontsPromise ??= Promise.resolve({}); 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 bf892b78..cb409474 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, type IEvent } from 'common/Event'; import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { disposableTimeout } from '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/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/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/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/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/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/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index bc84bded..c2f873c8 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -13,7 +13,8 @@ import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeS import { CharData, IBufferLine, ICellData } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; -import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { Terminal } from '@xterm/xterm'; import { GlyphRenderer } from './GlyphRenderer'; @@ -30,6 +31,7 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; private _cursorBlinkStateManager: MutableDisposable = new MutableDisposable(); + private _textBlinkStateManager: TextBlinkStateManager; private _charAtlasDisposable = this._register(new MutableDisposable()); private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; @@ -37,8 +39,9 @@ export class WebglRenderer extends Disposable implements IRenderer { private _observerDisposable = this._register(new MutableDisposable()); private _model: RenderModel = new RenderModel(); + private _rowHasBlinkingCells: boolean[] = []; + private _rowHasBlinkingCellsCount: number = 0; private _workCell: ICellData = new CellData(); - private _workCell2: ICellData = new CellData(); private _cellColorResolver: CellColorResolver; private _canvas: HTMLCanvasElement; @@ -105,6 +108,12 @@ export class WebglRenderer extends Disposable implements IRenderer { this._updateDimensions(); this._updateCursorBlink(); this._register(_optionsService.onOptionChange(() => this._handleOptionsChanged())); + this._textBlinkStateManager = this._register(new TextBlinkStateManager( + () => this._requestRedrawViewport(), + this._coreBrowserService, + this._optionsService + )); + this._resetBlinkingRowState(); this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE); @@ -178,6 +187,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._updateDimensions(); this._model.resize(this._terminal.cols, this._terminal.rows); + this._resetBlinkingRowState(); // Resize all render layers for (const l of this._renderLayers) { @@ -231,6 +241,10 @@ export class WebglRenderer extends Disposable implements IRenderer { this._requestRedrawViewport(); } + public handleViewportVisibilityChange(isVisible: boolean): void { + this._textBlinkStateManager.setViewportVisible(isVisible); + } + public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { for (const l of this._renderLayers) { l.handleSelectionChanged(this._terminal, start, end, columnSelectMode); @@ -323,6 +337,9 @@ export class WebglRenderer extends Disposable implements IRenderer { l.reset(this._terminal); } + this._resetBlinkingRowState(); + this._textBlinkStateManager.setNeedsBlinkInViewport(false); + this._cursorBlinkStateManager.value?.restartBlinkAnimation(); this._updateCursorBlink(); } @@ -420,6 +437,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (y = start; y <= end; y++) { row = y + terminal.buffer.ydisp; line = terminal.buffer.lines.get(row)!; + let rowHasBlinkingCells = false; this._model.lineLengths[y] = 0; isCursorRow = cursorY === row; skipJoinedCheckUntilX = 0; @@ -477,6 +495,10 @@ export class WebglRenderer extends Disposable implements IRenderer { code = cell.getCode(); i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; + if (!rowHasBlinkingCells && cell.isBlink()) { + rowHasBlinkingCells = true; + } + // Load colors/resolve overrides into work colors this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width); @@ -506,6 +528,10 @@ export class WebglRenderer extends Disposable implements IRenderer { } } + if (this._textBlinkStateManager.isEnabled && !this._textBlinkStateManager.isBlinkOn && cell.isBlink()) { + this._cellColorResolver.result.fg |= FgFlags.INVISIBLE; + } + if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; } @@ -552,11 +578,31 @@ export class WebglRenderer extends Disposable implements IRenderer { x--; // Go back to the previous update cell for next iteration } } + this._setRowBlinkState(y, rowHasBlinkingCells); } if (modelUpdated) { this._rectangleRenderer.value!.updateBackgrounds(this._model); } this._rectangleRenderer.value!.updateCursor(this._model); + this._updateTextBlinkState(); + } + + private _resetBlinkingRowState(): void { + this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false); + this._rowHasBlinkingCellsCount = 0; + } + + private _setRowBlinkState(row: number, hasBlinkingCells: boolean): void { + const previous = this._rowHasBlinkingCells[row]; + if (previous === hasBlinkingCells) { + return; + } + this._rowHasBlinkingCells[row] = hasBlinkingCells; + this._rowHasBlinkingCellsCount += hasBlinkingCells ? 1 : -1; + } + + private _updateTextBlinkState(): void { + this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount > 0); } /** 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 2665a398..1fcc6db0 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'; @@ -213,11 +216,14 @@ if (document.location.pathname === '/test') { 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,11 +235,14 @@ 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); + 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)); @@ -501,6 +510,12 @@ function initAddons(term: Terminal): void { 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 { @@ -517,6 +532,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; diff --git a/demo/client/components/window/addonImageWindow.ts b/demo/client/components/window/addonImageWindow.ts index 87ebaff7..ce5ab9de 100644 --- a/demo/client/components/window/addonImageWindow.ts +++ b/demo/client/components/window/addonImageWindow.ts @@ -5,6 +5,7 @@ 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'; @@ -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([ + '', + '-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-', + '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', + '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', + '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', + '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', + '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| ---> ->- >- >>-', - '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', - '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', - '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', - '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', - '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -|