Skip to content

Commit 735afde

Browse files
authored
Merge pull request #5663 from Tyriar/image_demo
Move remaining addon test buttons into their own windows
2 parents 3512f14 + 53efc95 commit 735afde

6 files changed

Lines changed: 362 additions & 244 deletions

File tree

demo/client/client.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ if ('WebAssembly' in window) {
1616
import { Terminal, ITerminalOptions, type ITheme } from '@xterm/xterm';
1717
import { AttachAddon } from '@xterm/addon-attach';
1818
import { AddonImageWindow } from './components/window/addonImageWindow';
19+
import { AddonLigaturesWindow } from './components/window/addonLigaturesWindow';
20+
import { AddonProgressWindow } from './components/window/addonProgressWindow';
1921
import { AddonSearchWindow } from './components/window/addonSearchWindow';
2022
import { AddonSerializeWindow } from './components/window/addonSerializeWindow';
2123
import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow';
24+
import { AddonWebLinksWindow } from './components/window/addonWebLinksWindow';
2225
import { AddonsWindow } from './components/window/addonsWindow';
2326
import { CellInspectorWindow } from './components/window/cellInspectorWindow';
2427
import { ControlBar } from './components/controlBar';
@@ -213,11 +216,14 @@ if (document.location.pathname === '/test') {
213216
controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons));
214217
controlBar.registerWindow(new VtWindow(typedTerm, addons));
215218
addonsWindow = controlBar.registerWindow(new AddonsWindow(typedTerm, addons));
216-
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
219+
controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
220+
controlBar.registerWindow(new AddonLigaturesWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
221+
controlBar.registerWindow(new AddonProgressWindow(typedTerm, addons), { afterId: 'addon-ligatures', hidden: true, italics: true });
222+
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addon-progress', hidden: true, italics: true });
217223
controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true });
218-
controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
219-
controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
220-
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
224+
controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
225+
controlBar.registerWindow(new AddonWebLinksWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
226+
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-links', hidden: true, italics: true });
221227
controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
222228
actionElements = {
223229
findNext: addonSearchWindow.findNextInput,
@@ -229,11 +235,14 @@ if (document.location.pathname === '/test') {
229235
// TODO: Most of below should be encapsulated within windows
230236
paddingElement = styleWindow.paddingElement;
231237

232-
controlBar.setTabVisible('addon-webgl', true);
238+
controlBar.setTabVisible('addon-image', !!addons.image.instance);
239+
controlBar.setTabVisible('addon-ligatures', !!addons.ligatures.instance);
240+
controlBar.setTabVisible('addon-progress', !!addons.progress.instance);
233241
controlBar.setTabVisible('addon-search', true);
234242
controlBar.setTabVisible('addon-serialize', true);
235-
controlBar.setTabVisible('addon-image', true);
236243
controlBar.setTabVisible('addon-web-fonts', true);
244+
controlBar.setTabVisible('addon-web-links', !!addons.webLinks.instance);
245+
controlBar.setTabVisible('addon-webgl', true);
237246
addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
238247
addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
239248
addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
@@ -500,6 +509,12 @@ function initAddons(term: Terminal): void {
500509
addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
501510
} else if (name === 'serialize') {
502511
controlBar.setTabVisible('addon-serialize', true);
512+
} else if (name === 'ligatures') {
513+
controlBar.setTabVisible('addon-ligatures', true);
514+
} else if (name === 'progress') {
515+
controlBar.setTabVisible('addon-progress', true);
516+
} else if (name === 'webLinks') {
517+
controlBar.setTabVisible('addon-web-links', true);
503518
}
504519
}
505520
catch {
@@ -516,6 +531,12 @@ function initAddons(term: Terminal): void {
516531
controlBar.setTabVisible('addon-search', false);
517532
} else if (name === 'serialize') {
518533
controlBar.setTabVisible('addon-serialize', false);
534+
} else if (name === 'ligatures') {
535+
controlBar.setTabVisible('addon-ligatures', false);
536+
} else if (name === 'progress') {
537+
controlBar.setTabVisible('addon-progress', false);
538+
} else if (name === 'webLinks') {
539+
controlBar.setTabVisible('addon-web-links', false);
519540
}
520541
addon.instance!.dispose();
521542
addon.instance = undefined;

demo/client/components/window/addonImageWindow.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { BaseWindow } from './baseWindow';
77
import type { IControlWindow } from '../controlBar';
8+
import type { IImageAddonOptions } from '@xterm/addon-image';
89

910
export class AddonImageWindow extends BaseWindow implements IControlWindow {
1011
public readonly id = 'addon-image';
@@ -46,6 +47,20 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
4647
this._imageOptionsTextarea.rows = 12;
4748
optionsLabel.appendChild(this._imageOptionsTextarea);
4849
container.appendChild(optionsLabel);
50+
51+
container.appendChild(document.createElement('br'));
52+
container.appendChild(document.createElement('br'));
53+
54+
const dl = document.createElement('dl');
55+
const dt = document.createElement('dt');
56+
dt.textContent = 'Image Test';
57+
dl.appendChild(dt);
58+
this._addDdWithButton(dl, 'image-demo1', 'snake (sixel)');
59+
this._addDdWithButton(dl, 'image-demo2', 'oranges (sixel)');
60+
this._addDdWithButton(dl, 'image-demo3', 'palette (iip)');
61+
container.appendChild(dl);
62+
63+
this._initImageAddonExposed();
4964
}
5065

5166
public get imageStorageLimitInput(): HTMLInputElement {
@@ -59,4 +74,93 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
5974
public get imageOptionsTextarea(): HTMLTextAreaElement {
6075
return this._imageOptionsTextarea;
6176
}
77+
78+
private _addDdWithButton(dl: HTMLElement, id: string, label: string): void {
79+
const dd = document.createElement('dd');
80+
const button = document.createElement('button');
81+
button.id = id;
82+
button.textContent = label;
83+
dd.appendChild(button);
84+
dl.appendChild(dd);
85+
}
86+
87+
private _initImageAddonExposed(): void {
88+
const imageAddon = this._addons.image.instance!;
89+
const defaultOptions: IImageAddonOptions = (imageAddon as any)._defaultOpts;
90+
const limitStorageElement = document.querySelector<HTMLInputElement>('#image-storagelimit')!;
91+
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
92+
this._addDomListener(limitStorageElement, 'change', () => {
93+
try {
94+
imageAddon.storageLimit = limitStorageElement.valueAsNumber;
95+
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
96+
console.log('changed storageLimit to', imageAddon.storageLimit);
97+
} catch (e) {
98+
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
99+
console.log('storageLimit at', imageAddon.storageLimit);
100+
throw e;
101+
}
102+
});
103+
const showPlaceholderElement = document.querySelector<HTMLInputElement>('#image-showplaceholder')!;
104+
showPlaceholderElement.checked = imageAddon.showPlaceholder;
105+
this._addDomListener(showPlaceholderElement, 'change', () => {
106+
imageAddon.showPlaceholder = showPlaceholderElement.checked;
107+
});
108+
const ctorOptionsElement = document.querySelector<HTMLTextAreaElement>('#image-options')!;
109+
ctorOptionsElement.value = JSON.stringify(defaultOptions, null, 2);
110+
111+
const sixelDemo = (url: string) => () => fetch(url)
112+
.then(resp => resp.arrayBuffer())
113+
.then(buffer => {
114+
this._terminal.write('\r\n');
115+
this._terminal.write(new Uint8Array(buffer));
116+
});
117+
118+
const iipDemo = (url: string) => () => fetch(url)
119+
.then(resp => resp.arrayBuffer())
120+
.then(buffer => {
121+
const data = new Uint8Array(buffer);
122+
let sdata = '';
123+
for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]);
124+
this._terminal.write('\r\n');
125+
this._terminal.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`);
126+
});
127+
128+
document.getElementById('image-demo1')!.addEventListener('click',
129+
sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six'));
130+
document.getElementById('image-demo2')!.addEventListener('click',
131+
sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
132+
document.getElementById('image-demo3')!.addEventListener('click',
133+
iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
134+
135+
// demo for image retrieval API
136+
this._terminal.element!.addEventListener('click', (ev: MouseEvent) => {
137+
if (!ev.ctrlKey || !imageAddon) return;
138+
139+
// TODO...
140+
// if (ev.altKey) {
141+
// const sel = term.getSelectionPosition();
142+
// if (sel) {
143+
// addons.image.instance
144+
// .extractCanvasAtBufferRange(term.getSelectionPosition())
145+
// ?.toBlob(data => window.open(URL.createObjectURL(data), '_blank'));
146+
// return;
147+
// }
148+
// }
149+
150+
const pos = (this._terminal as any)._core._mouseService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
151+
const x = pos[0] - 1;
152+
const y = pos[1] - 1;
153+
const canvas = ev.shiftKey
154+
// ctrl+shift+click: get single tile
155+
? imageAddon.extractTileAtBufferCell(x, this._terminal.buffer.active.viewportY + y)
156+
// ctrl+click: get original image
157+
: imageAddon.getImageAtBufferCell(x, this._terminal.buffer.active.viewportY + y);
158+
canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank'));
159+
});
160+
}
161+
162+
private _addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
163+
element.addEventListener(type, handler);
164+
(this._terminal as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
165+
}
62166
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import { BaseWindow } from './baseWindow';
7+
import type { IControlWindow } from '../controlBar';
8+
9+
export class AddonLigaturesWindow extends BaseWindow implements IControlWindow {
10+
public readonly id = 'addon-ligatures';
11+
public readonly label = 'ligatures';
12+
13+
public build(container: HTMLElement): void {
14+
const dl = document.createElement('dl');
15+
const dt = document.createElement('dt');
16+
dt.textContent = 'Ligatures Addon';
17+
dl.appendChild(dt);
18+
19+
const dd = document.createElement('dd');
20+
const button = document.createElement('button');
21+
button.id = 'ligatures-test';
22+
button.textContent = 'Common ligatures';
23+
button.title = 'Write common ligatures sequences';
24+
button.addEventListener('click', () => this._ligaturesTest());
25+
dd.appendChild(button);
26+
dl.appendChild(dd);
27+
28+
container.appendChild(dl);
29+
}
30+
31+
private _ligaturesTest(): void {
32+
this._terminal.write([
33+
'',
34+
'-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
35+
'=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
36+
'<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
37+
'<~~ </ </> /> ~~> == != /= ~= <> === !== !=== =/= =!=',
38+
'<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
39+
'(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| <!-- <!---',
40+
'==== ===== ====== ======= ======== =========',
41+
'---- ----- ------ ------- -------- ---------'
42+
].join('\r\n'));
43+
}
44+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import { BaseWindow } from './baseWindow';
7+
import type { IControlWindow } from '../controlBar';
8+
import type { IProgressState } from '@xterm/addon-progress';
9+
10+
export class AddonProgressWindow extends BaseWindow implements IControlWindow {
11+
public readonly id = 'addon-progress';
12+
public readonly label = 'progress';
13+
14+
public build(container: HTMLElement): void {
15+
const dl = document.createElement('dl');
16+
const dt = document.createElement('dt');
17+
dt.textContent = 'Progress Addon';
18+
dl.appendChild(dt);
19+
20+
this._addDdWithButton(dl, 'progress-run', 'full set run', '');
21+
this._addDdWithButton(dl, 'progress-0', 'state 0: remove', '');
22+
this._addDdWithButton(dl, 'progress-1', 'state 1: set 20%', '');
23+
this._addDdWithButton(dl, 'progress-2', 'state 2: error', '');
24+
this._addDdWithButton(dl, 'progress-3', 'state 3: indeterminate', '');
25+
this._addDdWithButton(dl, 'progress-4', 'state 4: pause', '');
26+
27+
const progressDd = document.createElement('dd');
28+
const progressDiv = document.createElement('div');
29+
progressDiv.id = 'progress-progress';
30+
const progressPercent = document.createElement('div');
31+
progressPercent.id = 'progress-percent';
32+
const progressIndeterminate = document.createElement('div');
33+
progressIndeterminate.id = 'progress-indeterminate';
34+
progressDiv.appendChild(progressPercent);
35+
progressDiv.appendChild(progressIndeterminate);
36+
progressDd.appendChild(progressDiv);
37+
dl.appendChild(progressDd);
38+
39+
const stateDd = document.createElement('dd');
40+
const stateDiv = document.createElement('div');
41+
stateDiv.id = 'progress-state';
42+
stateDiv.textContent = 'State:';
43+
stateDd.appendChild(stateDiv);
44+
dl.appendChild(stateDd);
45+
46+
container.appendChild(dl);
47+
48+
this._initProgress();
49+
this._addProgressStyles(container);
50+
}
51+
52+
private _addDdWithButton(dl: HTMLElement, id: string, label: string, title: string): void {
53+
const dd = document.createElement('dd');
54+
const button = document.createElement('button');
55+
button.id = id;
56+
button.textContent = label;
57+
if (title) {
58+
button.title = title;
59+
}
60+
dd.appendChild(button);
61+
dl.appendChild(dd);
62+
}
63+
64+
private _initProgress(): void {
65+
const states = { 0: 'remove', 1: 'set', 2: 'error', 3: 'indeterminate', 4: 'pause' };
66+
const colors = { 0: '', 1: 'green', 2: 'red', 3: '', 4: 'yellow' };
67+
68+
function progressHandler({ state, value }: IProgressState): void {
69+
// Simulate windows taskbar hack by windows terminal:
70+
// Since the taskbar has no means to indicate error/pause state other than by coloring
71+
// the current progress, we move 0 to 10% and distribute higher values in the remaining 90 %
72+
// NOTE: This is most likely not what you want to do for other progress indicators,
73+
// that have a proper visual state for error/paused.
74+
value = Math.min(10 + value * 0.9, 100);
75+
document.getElementById('progress-percent')!.style.width = `${value}%`;
76+
document.getElementById('progress-percent')!.style.backgroundColor = colors[state];
77+
document.getElementById('progress-state')!.innerText = `State: ${states[state]}`;
78+
79+
document.getElementById('progress-percent')!.style.display = state === 3 ? 'none' : 'block';
80+
document.getElementById('progress-indeterminate')!.style.display = state === 3 ? 'block' : 'none';
81+
}
82+
83+
const progressAddon = this._addons.progress.instance!;
84+
progressAddon.onChange(progressHandler);
85+
86+
const initialProgress = progressAddon.progress;
87+
progressHandler(initialProgress);
88+
89+
document.getElementById('progress-run')!.addEventListener('click', async () => {
90+
this._terminal.write('\x1b]9;4;0\x1b\\');
91+
for (let i = 0; i <= 100; i += 5) {
92+
this._terminal.write(`\x1b]9;4;1;${i}\x1b\\`);
93+
await new Promise(res => setTimeout(res, 200));
94+
}
95+
});
96+
document.getElementById('progress-0')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;0\x1b\\'));
97+
document.getElementById('progress-1')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;1;20\x1b\\'));
98+
document.getElementById('progress-2')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;2\x1b\\'));
99+
document.getElementById('progress-3')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;3\x1b\\'));
100+
document.getElementById('progress-4')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;4\x1b\\'));
101+
}
102+
103+
private _addProgressStyles(container: HTMLElement): void {
104+
const style = document.createElement('style');
105+
style.textContent = `
106+
#progress-progress {
107+
border: 1px solid black;
108+
height: 10px;
109+
}
110+
#progress-percent {
111+
height: 100%;
112+
}
113+
#progress-indeterminate {
114+
display: none;
115+
position: relative;
116+
height: 100%;
117+
}
118+
#progress-indeterminate:before {
119+
content: '';
120+
position: absolute;
121+
left: 0;
122+
bottom: 0px;
123+
width: 50px;
124+
height: 10px;
125+
background: blue;
126+
animation: ballbns 1s ease-in-out infinite alternate;
127+
}
128+
@keyframes ballbns {
129+
0% { left: 0; transform: translateX(0%); }
130+
100% { left: 100%; transform: translateX(-100%); }
131+
}
132+
`;
133+
container.appendChild(style);
134+
}
135+
}

0 commit comments

Comments
 (0)