/** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ 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; public build(container: HTMLElement): void { // Storage limit const storageLimitLabel = document.createElement('label'); storageLimitLabel.textContent = 'Storage Limit (in MB) '; this._imageStorageLimitInput = document.createElement('input'); this._imageStorageLimitInput.type = 'number'; this._imageStorageLimitInput.id = 'image-storagelimit'; storageLimitLabel.appendChild(this._imageStorageLimitInput); container.appendChild(storageLimitLabel); container.appendChild(document.createElement('br')); // Show placeholder const placeholderLabel = document.createElement('label'); placeholderLabel.textContent = 'Show Placeholder '; this._imageShowPlaceholderCheckbox = document.createElement('input'); this._imageShowPlaceholderCheckbox.type = 'checkbox'; this._imageShowPlaceholderCheckbox.id = 'image-showplaceholder'; placeholderLabel.appendChild(this._imageShowPlaceholderCheckbox); container.appendChild(placeholderLabel); container.appendChild(document.createElement('br')); container.appendChild(document.createElement('br')); // Ctor options const optionsLabel = document.createElement('label'); optionsLabel.appendChild(document.createTextNode('Ctor options (applied on addon relaunch)')); optionsLabel.appendChild(document.createElement('br')); this._imageOptionsTextarea = document.createElement('textarea'); this._imageOptionsTextarea.id = 'image-options'; this._imageOptionsTextarea.cols = 40; this._imageOptionsTextarea.rows = 12; optionsLabel.appendChild(this._imageOptionsTextarea); container.appendChild(optionsLabel); container.appendChild(document.createElement('br')); container.appendChild(document.createElement('br')); // Sixel demos const dlSixel = document.createElement('dl'); const dtSixel = document.createElement('dt'); dtSixel.textContent = 'Sixel'; dlSixel.appendChild(dtSixel); this._addDdWithButton(dlSixel, 'image-demo1', 'snake'); this._addDdWithButton(dlSixel, 'image-demo2', 'oranges'); container.appendChild(dlSixel); // IIP demos const dlIip = document.createElement('dl'); const dtIip = document.createElement('dt'); dtIip.textContent = 'IIP (iTerm)'; dlIip.appendChild(dtIip); this._addDdWithButton(dlIip, 'image-demo3', 'palette (png File)'); this._addDdWithButton(dlIip, 'image-demo4', 'dice (qoi MultipartFile)'); this._addDdWithButton(dlIip, 'image-demo5', 'rose (webp File)'); this._addDdWithButton(dlIip, 'image-demo6', 'kimono (avif MultipartFile)'); container.appendChild(dlIip); // Kitty demos const dlKitty = document.createElement('dl'); const dtKitty = document.createElement('dt'); dtKitty.textContent = 'Kitty'; dlKitty.appendChild(dtKitty); this._addDdWithButton(dlKitty, 'image-demo-kitty1', 'palette'); container.appendChild(dlKitty); this._initImageAddonExposed(); } public get imageStorageLimitInput(): HTMLInputElement { return this._imageStorageLimitInput; } public get imageShowPlaceholderCheckbox(): HTMLInputElement { return this._imageShowPlaceholderCheckbox; } 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\\`); }); const iipDemoMulti = (url: string) => () => fetch(url) .then(resp => resp.arrayBuffer()) .then(buffer => { const data = new Uint8Array(buffer); let sdata = ''; for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]); const encoded = btoa(sdata); this._terminal.write('\r\n'); this._terminal.write(`\x1b]1337;MultipartFile=inline=1\x1b\\`); for (let i = 0; i < encoded.length; i += 100) { this._terminal.write(`\x1b]1337;FilePart=${encoded.slice(i, i + 100)}\x1b\\`); } this._terminal.write(`\x1b]1337;FileEnd\x1b\\`); }); const kittyDemo = (url: string) => () => fetch(url) .then(resp => resp.arrayBuffer()) .then(buffer => { const data = new Uint8Array(buffer); let sdata = ''; for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]); const payload = btoa(sdata); this._terminal.write('\r\n'); this._terminal.write(`\x1b_Ga=T,f=100;${payload}\x1b\\`); }); document.getElementById('image-demo1')!.addEventListener('click', sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six')); document.getElementById('image-demo2')!.addEventListener('click', sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel')); document.getElementById('image-demo3')!.addEventListener('click', iipDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/palette.png')); document.getElementById('image-demo4')!.addEventListener('click', iipDemoMulti('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/dice.qoi')); document.getElementById('image-demo5')!.addEventListener('click', iipDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/1_webp_a.webp')); document.getElementById('image-demo6')!.addEventListener('click', iipDemoMulti('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/kimono.crop.avif')); document.getElementById('image-demo-kitty1')!.addEventListener('click', kittyDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/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._mouseCoordsService!.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) }); } }