/** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT * * This file is the entry point for browserify. */ /// /* eslint-disable no-restricted-syntax */ // HACK: Playwright/WebKit on Windows does not support WebAssembly https://stackoverflow.com/q/62311688/1156119 import type { ImageAddon as ImageAddonType, IImageAddonOptions } from '@xterm/addon-image'; let ImageAddon: typeof ImageAddonType | undefined; // eslint-disable-line @typescript-eslint/naming-convention if ('WebAssembly' in window) { const imageAddon = require('@xterm/addon-image'); ImageAddon = imageAddon.ImageAddon; } import { Terminal, ITerminalOptions, type IDisposable, type ITheme } from '@xterm/xterm'; import { AttachAddon } from '@xterm/addon-attach'; import { AddonsWindow } from './components/window/addonsWindow'; import { ControlBar } from './components/controlBar'; import { GpuWindow } from './components/window/gpuWindow'; import { OptionsWindow } from './components/window/optionsWindow'; import { StyleWindow } from './components/window/styleWindow'; import { TestWindow } from './components/window/testWindow'; import { VtWindow } from './components/window/vtWindow'; import { ClipboardAddon } from '@xterm/addon-clipboard'; import { FitAddon } from '@xterm/addon-fit'; import { LigaturesAddon } from '@xterm/addon-ligatures'; import { ProgressAddon, IProgressState } from '@xterm/addon-progress'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebglAddon } from '@xterm/addon-webgl'; import { Unicode11Addon } from '@xterm/addon-unicode11'; import { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; import { AddonCollection, type AddonType, type IDemoAddon } from './types'; export interface IWindowWithTerminal extends Window { term: typeof Terminal; Terminal: typeof Terminal; AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention ClipboardAddon?: typeof ClipboardAddon; // eslint-disable-line @typescript-eslint/naming-convention FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention ImageAddon?: typeof ImageAddon; // eslint-disable-line @typescript-eslint/naming-convention ProgressAddon?: typeof ProgressAddon; // eslint-disable-line @typescript-eslint/naming-convention SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention WebglAddon?: typeof WebglAddon; // eslint-disable-line @typescript-eslint/naming-convention Unicode11Addon?: typeof Unicode11Addon; // eslint-disable-line @typescript-eslint/naming-convention UnicodeGraphemesAddon?: typeof UnicodeGraphemesAddon; // eslint-disable-line @typescript-eslint/naming-convention LigaturesAddon?: typeof LigaturesAddon; // eslint-disable-line @typescript-eslint/naming-convention } declare let window: IWindowWithTerminal; let term; let protocol; let socketURL; let socket; let pid; let controlBar: ControlBar; let addonsWindow: AddonsWindow; let gpuWindow: GpuWindow; let optionsWindow: OptionsWindow; let vtWindow: VtWindow; 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 }, progress: { name: 'progress', ctor: ProgressAddon, canChange: true }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true }, webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }, unicodeGraphemes: { name: 'unicodeGraphemes', ctor: UnicodeGraphemesAddon, canChange: true }, ligatures: { name: 'ligatures', ctor: LigaturesAddon, canChange: true } }; let terminalContainer = document.getElementById('terminal-container'); let actionElements: { findNext: HTMLInputElement; findPrevious: HTMLInputElement; findResults: HTMLElement; }; let paddingElement: HTMLInputElement; const xtermjsTheme = { foreground: '#F8F8F8', background: '#2D2E2C', selectionBackground: '#5DA5D533', selectionInactiveBackground: '#555555AA', black: '#1E1E1D', brightBlack: '#262625', red: '#CE5C5C', brightRed: '#FF7272', green: '#5BCC5B', brightGreen: '#72FF72', yellow: '#CCCC5B', brightYellow: '#FFFF72', blue: '#5D5DD3', brightBlue: '#7279FF', magenta: '#BC5ED1', brightMagenta: '#E572FF', cyan: '#5DA5D5', brightCyan: '#72F0FF', white: '#F8F8F8', brightWhite: '#FFFFFF' } satisfies ITheme; function setPadding(): void { term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; addons.fit.instance.fit(); } function getSearchOptions(): ISearchOptions { return { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? { matchBackground: '#232422', matchBorder: '#555753', matchOverviewRuler: '#555753', activeMatchBackground: '#ef2929', activeMatchBorder: '#ffffff', activeMatchColorOverviewRuler: '#ef2929' } : undefined }; } const disposeRecreateButtonHandler: () => void = () => { // If the terminal exists dispose of it, otherwise recreate it if (term) { term.dispose(); term = null; window.term = null; socket = null; addons.attach.instance = undefined; addons.clipboard.instance = undefined; addons.fit.instance = undefined; addons.image.instance = undefined; addons.search.instance = undefined; addons.serialize.instance = undefined; addons.unicode11.instance = undefined; addons.unicodeGraphemes.instance = undefined; addons.ligatures.instance = undefined; addons.webLinks.instance = undefined; addons.webgl.instance = undefined; document.getElementById('dispose').innerHTML = 'Recreate Terminal'; } else { createTerminal(); document.getElementById('dispose').innerHTML = 'Dispose terminal'; } }; const createNewWindowButtonHandler: () => void = () => { if (term) { disposeRecreateButtonHandler(); } const win = window.open(); terminalContainer = win.document.createElement('div'); terminalContainer.id = 'terminal-container'; win.document.body.appendChild(terminalContainer); // Stylesheets are needed to get the terminal in the popout window to render // correctly. We also need to wait for them to load before creating the // terminal, otherwise we will not compute the correct metrics when rendering. let pendingStylesheets = 0; for (const linkNode of document.querySelectorAll('head link[rel=stylesheet]')) { const newLink = document.createElement('link'); newLink.rel = 'stylesheet'; newLink.href = (linkNode as HTMLLinkElement).href; win.document.head.appendChild(newLink); pendingStylesheets++; newLink.addEventListener('load', () => { pendingStylesheets--; if (pendingStylesheets === 0) { createTerminal(); } }); } }; if (document.location.pathname === '/test') { window.Terminal = Terminal; window.AttachAddon = AttachAddon; window.ClipboardAddon = ClipboardAddon; window.FitAddon = FitAddon; window.ImageAddon = ImageAddon; window.ProgressAddon = ProgressAddon; window.SearchAddon = SearchAddon; window.SerializeAddon = SerializeAddon; window.Unicode11Addon = Unicode11Addon; window.UnicodeGraphemesAddon = UnicodeGraphemesAddon; window.LigaturesAddon = LigaturesAddon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; } else { const typedTerm = createTerminal(); controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), []); addonsWindow = new AddonsWindow(); controlBar.registerWindow(addonsWindow); actionElements = { findNext: addonsWindow.findNextInput, findPrevious: addonsWindow.findPreviousInput, findResults: addonsWindow.findResultsSpan }; gpuWindow = new GpuWindow(); controlBar.registerWindow(gpuWindow, { afterId: 'addons', hidden: true, smallTab: true }); optionsWindow = new OptionsWindow(updateTerminalSize, updateTerminalContainerBackground); const styleWindow = controlBar.registerWindow(new StyleWindow()); paddingElement = styleWindow.paddingElement; controlBar.registerWindow(optionsWindow); controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler })); vtWindow = new VtWindow(); controlBar.registerWindow(vtWindow); vtWindow.initTerminal(term); controlBar.activateDefaultTab(); // TODO: Most of below should be encapsulated within windows controlBar.setTabVisible('gpu', true); gpuWindow.setTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => gpuWindow.setTextureAtlas(e)); addons.webgl.instance.onAddTextureAtlasCanvas(e => gpuWindow.appendTextureAtlas(e)); addons.webgl.instance.onRemoveTextureAtlasCanvas(e => gpuWindow.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()); e.preventDefault(); } }); addDomListener(actionElements.findNext, 'input', (e) => { 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()); e.preventDefault(); } }); addDomListener(actionElements.findPrevious, 'input', (e) => { addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions()); }); addDomListener(actionElements.findNext, 'blur', (e) => { addons.search.instance.clearActiveDecoration(); }); addDomListener(actionElements.findPrevious, 'blur', (e) => { addons.search.instance.clearActiveDecoration(); }); document.getElementById('serialize').addEventListener('click', serializeButtonHandler); document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); initImageAddonExposed(); testEvents(); progressButtons(); } function createTerminal(): Terminal { // Clean terminal while (terminalContainer.children.length) { terminalContainer.removeChild(terminalContainer.children[0]); } const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; term = new Terminal({ allowProposedApi: true, windowsPty: isWindows ? { // In a real scenario, these values should be verified on the backend backend: 'conpty', buildNumber: 22621 } : undefined, fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"', theme: xtermjsTheme } as ITerminalOptions); // Load addons const typedTerm = term as Terminal; addons.search.instance = new SearchAddon(); addons.serialize.instance = new SerializeAddon(); addons.fit.instance = new FitAddon(); addons.image.instance = new ImageAddon(); addons.progress.instance = new ProgressAddon(); addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon(); addons.clipboard.instance = new ClipboardAddon(); try { // try to start with webgl renderer (might throw on older safari/webkit) addons.webgl.instance = new WebglAddon(); } catch (e) { console.warn(e); } addons.webLinks.instance = new WebLinksAddon(); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.image.instance); typedTerm.loadAddon(addons.progress.instance); typedTerm.loadAddon(addons.search.instance); typedTerm.loadAddon(addons.serialize.instance); typedTerm.loadAddon(addons.unicodeGraphemes.instance); typedTerm.loadAddon(addons.webLinks.instance); typedTerm.loadAddon(addons.clipboard.instance); window.term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { if (!pid) { return; } const cols = size.cols; const rows = size.rows; const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; fetch(url, { method: 'POST' }); }); protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; addons.fit.instance!.fit(); if (addons.webgl.instance) { try { typedTerm.loadAddon(addons.webgl.instance); term.open(terminalContainer); } catch (e) { console.warn('error during loading webgl addon:', e); addons.webgl.instance.dispose(); addons.webgl.instance = undefined; } } if (!typedTerm.element) { // webgl loading failed for some reason, attach with DOM renderer term.open(terminalContainer); } term.focus(); updateTerminalContainerBackground(); const resizeObserver = new ResizeObserver(entries => { if (optionsWindow.autoResize) { addons.fit.instance.fit(); } }); resizeObserver.observe(terminalContainer); // fit is called within a setTimeout, cols and rows need this. setTimeout(async () => { optionsWindow.initOptions(term, addDomListener); // Set terminal size again to set the specific dimensions on the demo updateTerminalSize(); const useRealTerminal = document.getElementById('use-real-terminal'); if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) { runFakeTerminal(); } else { const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }); const processId = await res.text(); pid = processId; socketURL += processId; socket = new WebSocket(socketURL); socket.onopen = runRealTerminal; socket.onclose = runFakeTerminal; socket.onerror = runFakeTerminal; } }, 0); return typedTerm; } function runRealTerminal(): void { addons.attach.instance = new AttachAddon(socket); term.loadAddon(addons.attach.instance); term._initialized = true; initAddons(term); } function runFakeTerminal(): void { if (term._initialized) { return; } term._initialized = true; initAddons(term); term.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.onKey((e: { key: string, domEvent: KeyboardEvent }) => { const ev = e.domEvent; const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; if (ev.keyCode === 13) { term.prompt(); } else if (ev.keyCode === 8) { // Do not delete the prompt if (term._core.buffer.x > 2) { term.write('\b \b'); } } else if (printable) { term.write(e.key); } }); } function updateTerminalContainerBackground(): void { const bg = term.options.theme?.background ?? '#000000'; terminalContainer.style.backgroundColor = bg; } function initAddons(term: Terminal): void { const fragment = document.createDocumentFragment(); function postInitWebgl(): void { controlBar.setTabVisible('gpu', true); setTimeout(() => { gpuWindow.setTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => gpuWindow.setTextureAtlas(e)); addons.webgl.instance.onAddTextureAtlasCanvas(e => gpuWindow.appendTextureAtlas(e)); }, 500); } function preDisposeWebgl(): void { controlBar.setTabVisible('gpu', false); if (addons.webgl.instance.textureAtlas) { addons.webgl.instance.textureAtlas.remove(); } } Object.keys(addons).forEach((name: AddonType) => { const addon = addons[name]; const checkbox = document.createElement('input') as HTMLInputElement; checkbox.type = 'checkbox'; checkbox.checked = !!addon.instance; if (!addon.canChange) { checkbox.disabled = true; } if (name === 'unicode11' && checkbox.checked) { term.unicode.activeVersion = '11'; } if (name === 'unicodeGraphemes' && checkbox.checked) { term.unicode.activeVersion = '15-graphemes'; } if (name === 'search' && checkbox.checked) { addons[name].instance.onDidChangeResults(e => updateFindResults(e)); } addDomListener(checkbox, 'change', () => { if (name === 'image') { if (checkbox.checked) { const ctorOptionsJson = document.querySelector('#image-options').value; addon.instance = ctorOptionsJson ? new addons[name].ctor(JSON.parse(ctorOptionsJson)) : new addons[name].ctor(); term.loadAddon(addon.instance); } else { addon.instance!.dispose(); addon.instance = undefined; } return; } if (checkbox.checked) { // HACK: Manually remove addons that cannot be changes addon.instance = new (addon as IDemoAddon>).ctor(); try { term.loadAddon(addon.instance); if (name === 'webgl') { postInitWebgl(); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; } else if (name === 'unicodeGraphemes') { term.unicode.activeVersion = '15-graphemes'; } else if (name === 'search') { addons[name].instance.onDidChangeResults(e => updateFindResults(e)); } } catch { addon.instance = undefined; checkbox.checked = false; checkbox.disabled = true; } } else { if (name === 'webgl') { preDisposeWebgl(); } else if (name === 'unicode11' || name === 'unicodeGraphemes') { term.unicode.activeVersion = '6'; } addon.instance!.dispose(); addon.instance = undefined; } if (name === 'ligatures') { // Recreate webgl when ligatures are toggled so texture atlas picks up any font feature // settings changes if (addons.webgl.instance) { preDisposeWebgl(); addons.webgl.instance.dispose(); const customGlyphsCheckbox = document.getElementById('webgl-custom-glyphs') as HTMLInputElement; addons.webgl.instance = new addons.webgl.ctor({ customGlyphs: customGlyphsCheckbox?.checked ?? true }); term.loadAddon(addons.webgl.instance); postInitWebgl(); } } }); const label = document.createElement('label'); label.classList.add('addon'); if (!addon.canChange) { label.title = 'This addon is needed for the demo to operate'; } label.appendChild(checkbox); label.appendChild(document.createTextNode(name)); const wrapper = document.createElement('div'); wrapper.classList.add('addon'); wrapper.appendChild(label); // Add customGlyphs sub-checkbox for webgl addon if (name === 'webgl') { const customGlyphsCheckbox = document.createElement('input') as HTMLInputElement; customGlyphsCheckbox.type = 'checkbox'; customGlyphsCheckbox.checked = true; // Default to enabled customGlyphsCheckbox.id = 'webgl-custom-glyphs'; addDomListener(customGlyphsCheckbox, 'change', () => { if (addons.webgl.instance) { preDisposeWebgl(); addons.webgl.instance.dispose(); addons.webgl.instance = new addons.webgl.ctor({ customGlyphs: customGlyphsCheckbox.checked }); term.loadAddon(addons.webgl.instance); postInitWebgl(); } }); const customGlyphsLabel = document.createElement('label'); customGlyphsLabel.classList.add('addon'); customGlyphsLabel.style.display = 'block'; customGlyphsLabel.style.marginLeft = '20px'; customGlyphsLabel.appendChild(customGlyphsCheckbox); customGlyphsLabel.appendChild(document.createTextNode('customGlyphs')); wrapper.appendChild(customGlyphsLabel); } fragment.appendChild(wrapper); }); const container = addonsWindow.addonsContainer; container.innerHTML = ''; container.appendChild(fragment); } function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined): void { let content: string; if (e === undefined) { content = 'undefined'; } else { content = `index: ${e.resultIndex}, count: ${e.resultCount}`; } actionElements.findResults.textContent = content; } function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void { element.addEventListener(type, handler); term._core._register({ dispose: () => element.removeEventListener(type, handler) }); } function updateTerminalSize(): void { const width = optionsWindow.autoResize ? '100%' : (term._core._renderService.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px'; const height = optionsWindow.autoResize ? '100%' : (term._core._renderService.dimensions.css.canvas.height).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; addons.fit.instance.fit(); } function serializeButtonHandler(): void { const output = addons.serialize.instance.serialize(); const outputString = JSON.stringify(output); document.getElementById('serialize-output').innerText = outputString; if ((document.getElementById('write-to-terminal') as HTMLInputElement).checked) { term.reset(); term.write(output); } } function htmlSerializeButtonHandler(): void { const output = addons.serialize.instance.serializeAsHTML(); document.getElementById('htmlserialize-output').innerText = output; // Deprecated, but the most supported for now. function listener(e: any): void { e.clipboardData.setData('text/html', output); e.preventDefault(); } document.addEventListener('copy', listener); document.execCommand('copy'); document.removeEventListener('copy', listener); document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard'; } (console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => { function getBox(width: number, height: number): any { return { string: '+', style: 'font-size: 1px; padding: ' + Math.floor(height/2) + 'px ' + Math.floor(width/2) + 'px; line-height: ' + height + 'px;' }; } if (source instanceof HTMLCanvasElement) { source = source.getContext('2d')?.getImageData(0, 0, source.width, source.height)!; } const canvas = document.createElement('canvas'); canvas.width = source.width; canvas.height = source.height; const ctx = canvas.getContext('2d')!; ctx.putImageData(source, 0, 0); const sw = source.width * scale; const sh = source.height * scale; const dim = getBox(sw, sh); console.log( `Image: ${source.width} x ${source.height}\n%c${dim.string}`, `${dim.style}background: url(${canvas.toDataURL()}); background-size: ${sw}px ${sh}px; background-repeat: no-repeat; color: transparent;` ); console.groupCollapsed('Zoomed'); console.log( `%c${dim.string}`, `${getBox(sw * 10, sh * 10).style}background: url(${canvas.toDataURL()}); background-size: ${sw * 10}px ${sh * 10}px; background-repeat: no-repeat; color: transparent; image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;` ); console.groupEnd(); }; function initImageAddonExposed(): void { const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts; const limitStorageElement = document.querySelector('#image-storagelimit'); limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; addDomListener(limitStorageElement, 'change', () => { try { addons.image.instance.storageLimit = limitStorageElement.valueAsNumber; limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; console.log('changed storageLimit to', addons.image.instance.storageLimit); } catch (e) { limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; console.log('storageLimit at', addons.image.instance.storageLimit); throw e; } }); const showPlaceholderElement = document.querySelector('#image-showplaceholder'); showPlaceholderElement.checked = addons.image.instance.showPlaceholder; addDomListener(showPlaceholderElement, 'change', () => { addons.image.instance.showPlaceholder = showPlaceholderElement.checked; }); const ctorOptionsElement = document.querySelector('#image-options'); ctorOptionsElement.value = JSON.stringify(DEFAULT_OPTIONS, null, 2); const sixelDemo = (url: string) => () => fetch(url) .then(resp => resp.arrayBuffer()) .then(buffer => { term.write('\r\n'); term.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]); term.write('\r\n'); term.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 term.element.addEventListener('click', (ev: MouseEvent) => { if (!ev.ctrlKey || !addons.image.instance) 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 = term._core._mouseService!.getCoords(ev, term._core.screenElement!, term.cols, term.rows); const x = pos[0] - 1; const y = pos[1] - 1; const canvas = ev.shiftKey // ctrl+shift+click: get single tile ? addons.image.instance.extractTileAtBufferCell(x, term.buffer.active.viewportY + y) // ctrl+click: get original image : addons.image.instance.getImageAtBufferCell(x, term.buffer.active.viewportY + y); canvas?.toBlob(data => window.open(URL.createObjectURL(data), '_blank')); }); } function testEvents(): void { document.getElementById('event-focus').addEventListener('click', ()=> term.focus()); document.getElementById('event-blur').addEventListener('click', ()=> term.blur()); } function progressButtons(): void { const STATES = { 0: 'remove', 1: 'set', 2: 'error', 3: 'indeterminate', 4: 'pause' }; const COLORS = { 0: '', 1: 'green', 2: 'red', 3: '', 4: 'yellow' }; function progressHandler({ state, value }: IProgressState): void { // Simulate windows taskbar hack by windows terminal: // Since the taskbar has no means to indicate error/pause state other than by coloring // the current progress, we move 0 to 10% and distribute higher values in the remaining 90 % // NOTE: This is most likely not what you want to do for other progress indicators, // that have a proper visual state for error/paused. value = Math.min(10 + value * 0.9, 100); document.getElementById('progress-percent').style.width = `${value}%`; document.getElementById('progress-percent').style.backgroundColor = COLORS[state]; document.getElementById('progress-state').innerText = `State: ${STATES[state]}`; document.getElementById('progress-percent').style.display = state === 3 ? 'none' : 'block'; document.getElementById('progress-indeterminate').style.display = state === 3 ? 'block' : 'none'; } const progressAddon = addons.progress.instance; progressAddon.onChange(progressHandler); // apply initial state once to make it visible on page load const initialProgress = progressAddon.progress; progressHandler(initialProgress); document.getElementById('progress-run').addEventListener('click', async () => { term.write('\x1b]9;4;0\x1b\\'); for (let i = 0; i <= 100; i += 5) { term.write(`\x1b]9;4;1;${i}\x1b\\`); await new Promise(res => setTimeout(res, 200)); } }); document.getElementById('progress-0').addEventListener('click', () => term.write('\x1b]9;4;0\x1b\\')); document.getElementById('progress-1').addEventListener('click', () => term.write('\x1b]9;4;1;20\x1b\\')); document.getElementById('progress-2').addEventListener('click', () => term.write('\x1b]9;4;2\x1b\\')); document.getElementById('progress-3').addEventListener('click', () => term.write('\x1b]9;4;3\x1b\\')); document.getElementById('progress-4').addEventListener('click', () => term.write('\x1b]9;4;4\x1b\\')); }