From 53efc951700dd2cdccc7dc9839632aa484da5b71 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 1 Feb 2026 08:26:20 -0800 Subject: [PATCH] Move remaining addon test buttons into their own windows --- demo/client/client.ts | 33 ++- .../components/window/addonImageWindow.ts | 104 ++++++++ .../components/window/addonLigaturesWindow.ts | 44 ++++ .../components/window/addonProgressWindow.ts | 135 ++++++++++ .../components/window/addonWebLinksWindow.ts | 52 ++++ demo/client/components/window/testWindow.ts | 238 ------------------ 6 files changed, 362 insertions(+), 244 deletions(-) create mode 100644 demo/client/components/window/addonLigaturesWindow.ts create mode 100644 demo/client/components/window/addonProgressWindow.ts create mode 100644 demo/client/components/window/addonWebLinksWindow.ts diff --git a/demo/client/client.ts b/demo/client/client.ts index 45bdd1d4..43fa1413 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)); @@ -500,6 +509,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 { @@ -516,6 +531,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([ + '', + '-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-', + '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', + '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', + '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', + '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', + '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| ---> ->- >- >>-', - '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', - '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', - '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', - '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', - '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -|