/* eslint-disable no-restricted-syntax */ /** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT * * This file is the entry point for browserify. */ /// // 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 { ClipboardAddon } from '@xterm/addon-clipboard'; import { FitAddon } from '@xterm/addon-fit'; import { LigaturesAddon } from '@xterm/addon-ligatures'; 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'; 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 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 autoResize: boolean = true; type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures'; interface IDemoAddon { name: T; canChange: boolean; ctor: ( T extends 'attach' ? typeof AttachAddon : T extends 'clipboard' ? typeof ClipboardAddon : T extends 'fit' ? typeof FitAddon : T extends 'image' ? typeof ImageAddonType : T extends 'ligatures' ? typeof LigaturesAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : T extends 'webLinks' ? typeof WebLinksAddon : T extends 'unicode11' ? typeof Unicode11Addon : T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : T extends 'webgl' ? typeof WebglAddon : never ); instance?: ( T extends 'attach' ? AttachAddon : T extends 'clipboard' ? ClipboardAddon : T extends 'fit' ? FitAddon : T extends 'image' ? ImageAddonType : T extends 'ligatures' ? LigaturesAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : T extends 'webLinks' ? WebLinksAddon : T extends 'unicode11' ? Unicode11Addon : T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : T extends 'webgl' ? WebglAddon : never ); } const addons: { [T in AddonType]: IDemoAddon } = { 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 }, 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'); const actionElements = { find: document.querySelector('#find') as HTMLInputElement, findNext: document.querySelector('#find-next') as HTMLInputElement, findPrevious: document.querySelector('#find-previous') as HTMLInputElement, findResults: document.querySelector('#find-results') }; const paddingElement = document.getElementById('padding') as 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.SearchAddon = SearchAddon; window.SerializeAddon = SerializeAddon; window.Unicode11Addon = Unicode11Addon; window.UnicodeGraphemesAddon = UnicodeGraphemesAddon; window.LigaturesAddon = LigaturesAddon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; } else { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('create-new-window').addEventListener('click', createNewWindowButtonHandler); document.getElementById('serialize').addEventListener('click', serializeButtonHandler); document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); document.getElementById('load-test-long-lines').addEventListener('click', loadTestLongLines); document.getElementById('print-cjk').addEventListener('click', addCjk); document.getElementById('print-cjk-sgr').addEventListener('click', addCjkRandomSgr); document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest); document.getElementById('underline-test').addEventListener('click', underlineTest); document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest); document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink); document.getElementById('sgr-test').addEventListener('click', sgrTest); document.getElementById('add-grapheme-clusters').addEventListener('click', addGraphemeClusters); document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest); document.getElementById('ligatures-test').addEventListener('click', ligaturesTest); document.getElementById('weblinks-test').addEventListener('click', testWeblinks); document.getElementById('bce').addEventListener('click', coloredErase); addVtButtons(); initImageAddonExposed(); testEvents(); } function createTerminal(): void { // 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.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.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); setTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); addons.webgl.instance.onRemoveTextureAtlasCanvas(e => removeTextureAtlas(e)); } 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(); const resizeObserver = new ResizeObserver(entries => { if (autoResize) { addons.fit.instance.fit(); } }); resizeObserver.observe(terminalContainer); 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(); }); // fit is called within a setTimeout, cols and rows need this. setTimeout(async () => { initOptions(term); paddingElement.value = '0'; // 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); } 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 initOptions(term: Terminal): void { const blacklistedOptions = [ // Internal only options 'cancelEvents', 'convertEol', 'termName', 'cols', 'rows', // subsumed by "size" (colsRows) option // Complex option 'documentOverride', 'linkHandler', 'logger', 'overviewRuler', 'theme', 'windowOptions', 'windowsPty', // Deprecated 'fastScrollModifier' ]; const stringOptions = { cursorStyle: ['block', 'underline', 'bar'], cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], logLevel: ['trace', 'debug', 'info', 'warn', 'error', 'off'], theme: ['default', 'xtermjs', 'sapphire', 'light'], wordSeparator: null, colsRows: null }; const options = Object.getOwnPropertyNames(term.options); const booleanOptions = []; const numberOptions = []; options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => { switch (typeof term.options[o]) { case 'boolean': booleanOptions.push(o); break; case 'number': numberOptions.push(o); break; default: if (Object.keys(stringOptions).indexOf(o) === -1 && numberOptions.indexOf(o) === -1 && booleanOptions.indexOf(o) === -1) { console.warn(`Unrecognized option: "${o}"`); } } }); let html = ''; html += '
'; booleanOptions.forEach(o => { html += `
`; }); html += '
'; numberOptions.forEach(o => { html += `
`; }); html += '
'; Object.keys(stringOptions).forEach(o => { if (o === 'colsRows') { html += `
`; } else if (stringOptions[o]) { const selectedOption = o === 'theme' ? 'xtermjs' : term.options[o]; html += `
`; } else { html += `
`; } }); html += '
'; const container = document.getElementById('options-container'); container.innerHTML = html; // Attach listeners booleanOptions.forEach(o => { const input = document.getElementById(`opt-${o}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', o, input.checked); term.options[o] = input.checked; }); }); numberOptions.forEach(o => { const input = document.getElementById(`opt-${o}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', o, input.value); if (o === 'lineHeight') { term.options.lineHeight = parseFloat(input.value); } else if (o === 'scrollSensitivity') { term.options.scrollSensitivity = parseFloat(input.value); } else if (o === 'scrollback') { term.options.scrollback = parseInt(input.value); setTimeout(() => updateTerminalSize(), 5); } else { term.options[o] = parseInt(input.value); } // Always update terminal size in case the option changes the dimensions updateTerminalSize(); }); }); Object.keys(stringOptions).forEach(o => { const input = document.getElementById(`opt-${o}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', o, input.value); let value: any = input.value; if (o === 'colsRows') { const m = input.value.match(/^([0-9]+)x([0-9]+)$/); if (m) { autoResize = false; term.resize(parseInt(m[1]), parseInt(m[2])); } else { autoResize = true; input.value = 'auto'; updateTerminalSize(); } } else if (o === 'theme') { switch (input.value) { case 'default': value = undefined; break; case 'xtermjs': // Custom theme to match style of xterm.js logo value = xtermjsTheme; case 'sapphire': // Color source: https://github.com/Tyriar/vscode-theme-sapphire value = { background: '#1c2431', foreground: '#cccccc', selectionBackground: '#399ef440', black: '#666666', blue: '#399ef4', brightBlack: '#666666', brightBlue: '#399ef4', brightCyan: '#21c5c7', brightGreen: '#4eb071', brightMagenta: '#b168df', brightRed: '#da6771', brightWhite: '#efefef', brightYellow: '#fff099', cyan: '#21c5c7', green: '#4eb071', magenta: '#b168df', red: '#da6771', white: '#efefef', yellow: '#fff099' }; break; case 'light': // Color source: https://github.com/microsoft/vscode/blob/main/extensions/theme-defaults/themes/light_plus.json value = { background: '#ffffff', foreground: '#333333', cursor: '#333333', cursorAccent: '#ffffff', selectionBackground: '#add6ff', overviewRulerBorder: '#aaaaaa', black: '#000000', blue: '#0451a5', brightBlack: '#666666', brightBlue: '#0451a5', brightCyan: '#0598bc', brightGreen: '#14ce14', brightMagenta: '#bc05bc', brightRed: '#cd3131', brightWhite: '#a5a5a5', brightYellow: '#b5ba00', cyan: '#0598bc', green: '#00bc00', magenta: '#bc05bc', red: '#cd3131', white: '#555555', yellow: '#949800' }; break; } } term.options[o] = value; }); }); } function initAddons(term: Terminal): void { const fragment = document.createDocumentFragment(); 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; } function postInitWebgl(): void { setTimeout(() => { setTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); }, 500); } function preDisposeWebgl(): void { if (addons.webgl.instance.textureAtlas) { addons.webgl.instance.textureAtlas.remove(); } } 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(); addons.webgl.instance = new addons.webgl.ctor(); 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); fragment.appendChild(wrapper); }); const container = document.getElementById('addons-container'); 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 = autoResize ? '100%' : (term._core._renderService.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px'; const height = 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'; } function setTextureAtlas(e: HTMLCanvasElement): void { styleAtlasPage(e); document.querySelector('#texture-atlas').replaceChildren(e); } function appendTextureAtlas(e: HTMLCanvasElement): void { styleAtlasPage(e); document.querySelector('#texture-atlas').appendChild(e); } function removeTextureAtlas(e: HTMLCanvasElement): void { e.remove(); } function styleAtlasPage(e: HTMLCanvasElement): void { e.style.width = `${e.width / window.devicePixelRatio}px`; e.style.height = `${e.height / window.devicePixelRatio}px`; } function writeCustomGlyphHandler(): void { term.write('\n\r'); term.write('\n\r'); term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r'); term.write('┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤\n\r'); term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘\n\r'); term.write('├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐\n\r'); term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤\n\r'); term.write('└─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘\n\r'); term.write('\n\r'); term.write('Other:\n\r'); term.write('╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈\n\r'); term.write('│ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉\n\r'); term.write('╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋\n\r'); term.write('\n\r'); term.write('All box drawing characters:\n\r'); term.write('─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏\n\r'); term.write('┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟\n\r'); term.write('┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯\n\r'); term.write('┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿\n\r'); term.write('╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏\n\r'); term.write('═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟\n\r'); term.write('╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯\n\r'); term.write('╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿\n\r'); term.write('Box drawing alignment tests:\x1b[31m █\n\r'); term.write(' ▉\n\r'); term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r'); term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r'); term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r'); term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r'); term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); term.write('Box drawing alignment tests:\x1b[32m █\n\r'); term.write(' ▉\n\r'); term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r'); term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r'); term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r'); term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r'); term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); term.write('\x1b[0m'); window.scrollTo(0, 0); } function loadTest(): void { const rendererName = addons.webgl.instance ? 'webgl' : 'dom'; const testData = []; let byteCount = 0; for (let i = 0; i < 50; i++) { const count = 1 + Math.floor(Math.random() * 79); byteCount += count + 2; const data = new Uint8Array(count + 2); data[0] = 0x0A; // \n for (let i = 1; i < count + 1; i++) { data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61)); } // End each line with \r so the cursor remains constant, this is what ls/tree do and improves // performance significantly due to the cursor DOM element not needing to change data[data.length - 1] = 0x0D; // \r testData.push(data); } const start = performance.now(); for (let i = 0; i < 1024; i++) { for (const d of testData) { term.write(d); } } // Wait for all data to be parsed before evaluating time term.write('', () => { const time = Math.round(performance.now() - start); const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`); // Send ^C to get a new prompt term._core._onData.fire('\x03'); }); } function loadTestLongLines(): void { const rendererName = addons.webgl.instance ? 'webgl' : 'dom'; const testData = []; let byteCount = 0; for (let i = 0; i < 50; i++) { const count = 1 + Math.floor(Math.random() * 500); byteCount += count + 2; const data = new Uint8Array(count + 2); data[0] = 0x0A; // \n for (let i = 1; i < count + 1; i++) { data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61)); } // End each line with \r so the cursor remains constant, this is what ls/tree do and improves // performance significantly due to the cursor DOM element not needing to change data[data.length - 1] = 0x0D; // \r testData.push(data); } const start = performance.now(); for (let i = 0; i < 1024 * 50; i++) { for (const d of testData) { term.write(d); } } // Wait for all data to be parsed before evaluating time term.write('', () => { const time = Math.round(performance.now() - start); const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`); // Send ^C to get a new prompt term._core._onData.fire('\x03'); }); } function powerlineSymbolTest(): void { function s(char: string): string { return `${char} \x1b[7m${char}\x1b[0m `; } term.write('\n\n\r'); term.writeln('Standard powerline symbols:'); term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); term.writeln(`0xA_ ${s('\ue0a0')}${s('\ue0a1')}${s('\ue0a2')}`); term.writeln(`0xB_ ${s('\ue0b0')}${s('\ue0b1')}${s('\ue0b2')}${s('\ue0b3')}`); term.writeln(''); term.writeln( `\x1b[7m` + ` inverse \ue0b1 \x1b[0;40m\ue0b0` + ` 0 \ue0b1 \x1b[30;41m\ue0b0\x1b[39m` + ` 1 \ue0b1 \x1b[31;42m\ue0b0\x1b[39m` + ` 2 \ue0b1 \x1b[32;43m\ue0b0\x1b[39m` + ` 3 \ue0b1 \x1b[33;44m\ue0b0\x1b[39m` + ` 4 \ue0b1 \x1b[34;45m\ue0b0\x1b[39m` + ` 5 \ue0b1 \x1b[35;46m\ue0b0\x1b[39m` + ` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[30m` + ` 7 \ue0b1 \x1b[37;49m\ue0b0\x1b[0m` ); term.writeln(''); term.writeln( `\x1b[7m` + ` inverse \ue0b3 \x1b[0;7;40m\ue0b2\x1b[27m` + ` 0 \ue0b3 \x1b[7;30;41m\ue0b2\x1b[27;39m` + ` 1 \ue0b3 \x1b[7;31;42m\ue0b2\x1b[27;39m` + ` 2 \ue0b3 \x1b[7;32;43m\ue0b2\x1b[27;39m` + ` 3 \ue0b3 \x1b[7;33;44m\ue0b2\x1b[27;39m` + ` 4 \ue0b3 \x1b[7;34;45m\ue0b2\x1b[27;39m` + ` 5 \ue0b3 \x1b[7;35;46m\ue0b2\x1b[27;39m` + ` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;30m` + ` 7 \ue0b3 \x1b[7;37;49m\ue0b2\x1b[0m` ); term.writeln(''); term.writeln( `\x1b[7m` + ` inverse \ue0b5 \x1b[0;40m\ue0b4` + ` 0 \ue0b5 \x1b[30;41m\ue0b4\x1b[39m` + ` 1 \ue0b5 \x1b[31;42m\ue0b4\x1b[39m` + ` 2 \ue0b5 \x1b[32;43m\ue0b4\x1b[39m` + ` 3 \ue0b5 \x1b[33;44m\ue0b4\x1b[39m` + ` 4 \ue0b5 \x1b[34;45m\ue0b4\x1b[39m` + ` 5 \ue0b5 \x1b[35;46m\ue0b4\x1b[39m` + ` 6 \ue0b5 \x1b[36;47m\ue0b4\x1b[30m` + ` 7 \ue0b5 \x1b[37;49m\ue0b4\x1b[0m` ); term.writeln(''); term.writeln( `\x1b[7m` + ` inverse \ue0b7 \x1b[0;7;40m\ue0b6\x1b[27m` + ` 0 \ue0b7 \x1b[7;30;41m\ue0b6\x1b[27;39m` + ` 1 \ue0b7 \x1b[7;31;42m\ue0b6\x1b[27;39m` + ` 2 \ue0b7 \x1b[7;32;43m\ue0b6\x1b[27;39m` + ` 3 \ue0b7 \x1b[7;33;44m\ue0b6\x1b[27;39m` + ` 4 \ue0b7 \x1b[7;34;45m\ue0b6\x1b[27;39m` + ` 5 \ue0b7 \x1b[7;35;46m\ue0b6\x1b[27;39m` + ` 6 \ue0b7 \x1b[7;36;47m\ue0b6\x1b[27;30m` + ` 7 \ue0b7 \x1b[7;37;49m\ue0b6\x1b[0m` ); term.writeln(''); term.writeln('Powerline extra symbols:'); term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); term.writeln(`0xA_ ${s('\ue0a3')}`); term.writeln(`0xB_ ${s('\ue0b4')}${s('\ue0b5')}${s('\ue0b6')}${s('\ue0b7')}${s('\ue0b8')}${s('\ue0b9')}${s('\ue0ba')}${s('\ue0bb')}${s('\ue0bc')}${s('\ue0bd')}${s('\ue0be')}${s('\ue0bf')}`); term.writeln(`0xC_ ${s('\ue0c0')}${s('\ue0c1')}${s('\ue0c2')}${s('\ue0c3')}${s('\ue0c4')}${s('\ue0c5')}${s('\ue0c6')}${s('\ue0c7')}${s('\ue0c8')}${s('\ue0c9')}${s('\ue0ca')}${s('\ue0cb')}${s('\ue0cc')}${s('\ue0cd')}${s('\ue0be')}${s('\ue0bf')}`); term.writeln(`0xD_ ${s('\ue0d0')}${s('\ue0d1')}${s('\ue0d2')} ${s('\ue0d4')}`); term.writeln(''); term.writeln('Sample of nerd fonts icons:'); term.writeln(' nf-linux-apple (\\uF302) \uf302'); term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9'); } function underlineTest(): void { function u(style: number): string { return `\x1b[4:${style}m`; } function c(color: string): string { return `\x1b[58:${color}m`; } term.write('\n\n\r'); term.writeln('Underline styles:'); term.writeln(''); function showSequence(id: number, name: string): string { let alphabet = ''; for (let i = 97; i < 123; i++) { alphabet += String.fromCharCode(i); } let numbers = ''; for (let i = 0; i < 10; i++) { numbers += i.toString(); } return `${u(id)}4:${id}m - ${name}\x1b[4:0m`.padEnd(33, ' ') + `${u(id)}${alphabet} ${numbers} 汉语 한국어 👽\x1b[4:0m`; } term.writeln(showSequence(0, 'No underline')); term.writeln(showSequence(1, 'Straight')); term.writeln(showSequence(2, 'Double')); term.writeln(showSequence(3, 'Curly')); term.writeln(showSequence(4, 'Dotted')); term.writeln(showSequence(5, 'Dashed')); term.writeln(''); term.writeln(`Underline colors (256 color mode):`); term.writeln(''); for (let i = 0; i < 256; i++) { term.write((i !== 0 ? '\x1b[0m, ' : '') + u(1 + i % 5) + c('5:' + i) + i); } term.writeln(`\x1b[0m\n\n\rUnderline colors (true color mode):`); term.writeln(''); for (let i = 0; i < 80; i++) { const v = Math.round(i / 79 * 255); term.write(u(1) + c(`2:0:${v}:${v}:${v}`) + (i < 4 ? 'grey'[i] : ' ')); } term.write('\n\r'); for (let i = 0; i < 80; i++) { const v = Math.round(i / 79 * 255); term.write(u(1) + c(`2:0:${v}:${0}:${0}`) + (i < 3 ? 'red'[i] : ' ')); } term.write('\n\r'); for (let i = 0; i < 80; i++) { const v = Math.round(i / 79 * 255); term.write(u(1) + c(`2:0:${0}:${v}:${0}`) + (i < 5 ? 'green'[i] : ' ')); } term.write('\n\r'); for (let i = 0; i < 80; i++) { const v = Math.round(i / 79 * 255); term.write(u(1) + c(`2:0:${0}:${0}:${v}`) + (i < 4 ? 'blue'[i] : ' ')); } term.write('\x1b[0m\n\r'); } function ansiColorsTest(): void { term.writeln(`\x1b[0m\n\n\rStandard colors: Bright colors:`); for (let i = 0; i < 16; i++) { term.write(`\x1b[48;5;${i}m ${i.toString().padEnd(2, ' ').padStart(3, ' ')} \x1b[0m`); } term.writeln(`\x1b[0m\n\n\rColors 17-231 from 256 palette:`); for (let i = 0; i < 6; i++) { const startId = 16 + i * 36; const endId = 16 + (i + 1) * 36 - 1; term.write(`${startId.toString().padStart(3, ' ')}-${endId.toString().padStart(3, ' ')} `); for (let j = 0; j < 36; j++) { const id = 16 + i * 36 + j; term.write(`\x1b[48;5;${id}m${(id % 10).toString().padStart(2, ' ')}\x1b[0m`); } term.write(`\r\n`); } term.writeln(`\x1b[0m\n\rGreyscale from 256 palette:`); term.write('232-255 '); for (let i = 232; i < 256; i++) { term.write(`\x1b[48;5;${i}m ${(i % 10)} \x1b[0m`); } } function writeTestString(): string { let alphabet = ''; for (let i = 97; i < 123; i++) { alphabet += String.fromCharCode(i); } let numbers = ''; for (let i = 0; i < 10; i++) { numbers += i.toString(); } return `${alphabet} ${numbers} 汉语 한국어 👽`; } const testString = writeTestString(); function sgrTest(): void { term.write('\n\n\r'); term.writeln(`Character Attributes (SGR, Select Graphic Rendition)`); const entries: { ps: number, name: string }[] = [ { ps: 0, name: 'Normal' }, { ps: 1, name: 'Bold' }, { ps: 2, name: 'Faint/dim' }, { ps: 3, name: 'Italicized' }, { ps: 4, name: 'Underlined' }, { ps: 5, name: 'Blink' }, { ps: 7, name: 'Inverse' }, { ps: 8, name: 'Invisible' }, { ps: 9, name: 'Crossed-out characters' }, { ps: 21, name: 'Doubly-underlined' }, { ps: 22, name: 'Normal' }, { ps: 23, name: 'Not italicized' }, { ps: 24, name: 'Not underlined' }, { ps: 25, name: 'Steady (not blink)' }, { ps: 27, name: 'Positive (not inverse)' }, { ps: 28, name: 'Visible (not hidden)' }, { ps: 29, name: 'Not crossed-out' }, { ps: 30, name: 'Foreground Black' }, { ps: 31, name: 'Foreground Red' }, { ps: 32, name: 'Foreground Green' }, { ps: 33, name: 'Foreground Yellow' }, { ps: 34, name: 'Foreground Blue' }, { ps: 35, name: 'Foreground Magenta' }, { ps: 36, name: 'Foreground Cyan' }, { ps: 37, name: 'Foreground White' }, { ps: 39, name: 'Foreground default' }, { ps: 40, name: 'Background Black' }, { ps: 41, name: 'Background Red' }, { ps: 42, name: 'Background Green' }, { ps: 43, name: 'Background Yellow' }, { ps: 44, name: 'Background Blue' }, { ps: 45, name: 'Background Magenta' }, { ps: 46, name: 'Background Cyan' }, { ps: 47, name: 'Background White' }, { ps: 49, name: 'Background default' }, { ps: 53, name: 'Overlined' }, { ps: 55, name: 'Not overlined' } ]; const maxNameLength = entries.reduce((p, c) => Math.max(c.name.length, p), 0); for (const e of entries) { term.writeln(`\x1b[0m\x1b[${e.ps}m ${e.ps.toString().padEnd(2, ' ')} ${e.name.padEnd(maxNameLength, ' ')} - ${testString}\x1b[0m`); } const entriesByPs: Map = new Map(); for (const e of entries) { entriesByPs.set(e.ps, e.name); } const comboEntries: { ps: number[] }[] = [ { ps: [1, 2, 3, 4, 5, 6, 7, 9] }, { ps: [2, 41] }, { ps: [4, 53] } ]; term.write('\n\n\r'); term.writeln(`Combinations`); for (const e of comboEntries) { const name = e.ps.map(e => entriesByPs.get(e)).join(', '); term.writeln(`\x1b[0m\x1b[${e.ps.join(';')}m ${name}\n\r${testString}\x1b[0m`); } } function addAnsiHyperlink(): void { term.write('\n\n\r'); term.writeln(`Regular link with no id:`); term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07'); term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); term.writeln(`\nAdjacent links:`); term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07\x1b[32mxterm.js\x1b[0m\x1b]8;;\x07'); term.writeln(`\nShared ID link (underline should be shared):`); term.writeln('╔════╗'); term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║'); term.writeln('║\x1b]8;id=testid;https://github.com\x07ub\x1b]8;;\x07 ║'); term.writeln('╚════╝'); term.writeln(`\nWrapped link with no ID (not necessarily meant to share underline):`); term.writeln('╔════╗'); term.writeln('║ ║'); term.writeln('║ ║'); term.writeln('╚════╝'); term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D'); } /** * Prints the 20977 characters from the CJK Unified Ideographs unicode block. */ function addCjk(): void { term.write('\n\n\r'); for (let i = 0x4E00; i < 0x9FCC; i++) { term.write(String.fromCharCode(i)); } } /** * Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized styles. */ function addCjkRandomSgr(): void { term.write('\n\n\r'); for (let i = 0x4E00; i < 0x9FCC; i++) { term.write(`\x1b[${getRandomSgr()}m${String.fromCharCode(i)}\x1b[0m`); } } const randomSgrAttributes = [ '1', '2', '3', '4', '5', '6', '7', '9', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49' ]; function getRandomSgr(): string { return randomSgrAttributes[Math.floor(Math.random() * randomSgrAttributes.length)]; } function addGraphemeClusters(): void { term.write('\n\n\r'); term.writeln('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣 [Simple emoji v6: 10 cells, v15: 20 cells]'); term.writeln('\u{1F476}\u{1F3FF}\u{1F476} [baby with emoji modifier fitzpatrick type-6; baby]'); term.writeln('\u{1F469}\u200d\u{1f469}\u200d\u{1f466} [woman+zwj+woman+zwj+boy]'); term.writeln('\u{1F64B}\u{1F64B}\u{200D}\u{2642}\u{FE0F} [person/man raising hand]'); term.writeln('\u{1F3CB}\u{FE0F}=\u{1F3CB}\u{1F3FE}\u{200D}\u{2640}\u{FE0F} [person lifting weights emoji; woman lighting weights, medium dark]'); term.writeln('\u{1F469}\u{1F469}\u{200D}\u{1F393}\u{1F468}\u{1F3FF}\u{200D}\u{1F393} [woman; woman student; man student dark]'); term.writeln('\u{1f1f3}\u{1f1f4}_ [REGIONAL INDICATOR SYMBOL LETTER N and RI O]'); term.writeln('\u{1f1f3}_\u{1f1f4} {RI N; underscore; RI O]'); term.writeln('\u0061\u0301 [letter a with acute accent]'); term.writeln('\u1100\u1161\u11A8=\u1100\u1161= [Korean Jamo]'); term.writeln('\uAC00=\uD685= [Hangul syllables (pre-composed)]'); term.writeln('(\u26b0\ufe0e) [coffin with text_presentation]'); term.writeln('(\u26b0\ufe0f) [coffin with Emoji_presentation]'); term.writeln(' [Égalité (using separate acute) emoij_presentation]'); } function addDecoration(): void { term.options['overviewRuler'] = { width: 14 }; const marker = term.registerMarker(1); const decoration = term.registerDecoration({ marker, backgroundColor: '#00FF00', foregroundColor: '#00FE00', overviewRulerOptions: { color: '#ef292980', position: 'left' } }); decoration.onRender((e: HTMLElement) => { e.style.right = '100%'; e.style.backgroundColor = '#ef292980'; }); } function addOverviewRuler(): void { term.options['overviewRuler'] = { width: 14 }; term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } }); term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } }); term.registerDecoration({ marker: term.registerMarker(5), overviewRulerOptions: { color: '#729fcf' } }); term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' } }); term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' } }); term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' } }); term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' } }); term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } }); } let decorationStressTestDecorations: IDisposable[] | undefined; function decorationStressTest(): void { if (decorationStressTestDecorations) { for (const d of decorationStressTestDecorations) { d.dispose(); } decorationStressTestDecorations = undefined; } else { const t = term as Terminal; const buffer = t.buffer.active; const cursorY = buffer.baseY + buffer.cursorY; decorationStressTestDecorations = []; for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) { for (let y = 0; y < t.buffer.active.length; y++) { const cursorOffsetY = y - cursorY; decorationStressTestDecorations.push(t.registerDecoration({ marker: t.registerMarker(cursorOffsetY), x, width: 4, backgroundColor: '#FF0000', overviewRulerOptions: { color: '#FF0000' } })); } } } } (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 addVtButtons(): void { function csi(e: string): string { return `\x1b[${e}`; } function createButton(name: string, description: string, writeCsi: string, paramCount: number = 1): HTMLElement { const inputs: HTMLInputElement[] = []; for (let i = 0; i < paramCount; i++) { const input = document.createElement('input'); input.type = 'number'; input.title = `Input #${i + 1}`; inputs.push(input); } const element = document.createElement('button'); element.textContent = name; const writeCsiSplit = writeCsi.split('|'); const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : ''; const suffix = writeCsiSplit[writeCsiSplit.length - 1]; element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`))); const desc = document.createElement('span'); desc.textContent = description; const container = document.createElement('div'); container.classList.add('vt-button'); container.append(element, ...inputs, desc); return container; } const vtFragment = document.createDocumentFragment(); const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = { A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' }, B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' }, C: { label: 'CUF →', description: 'Cursor Forward Ps Times' }, D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' }, E: { label: 'CNL', description: 'Cursor Next Line Ps Times' }, F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' }, G: { label: 'CHA', description: 'Cursor Character Absolute' }, H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 }, I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' }, J: { label: 'ED', description: 'Erase in Display' }, '?|J': { label: 'DECSED', description: 'Erase in Display' }, K: { label: 'EL', description: 'Erase in Line' }, '?|K': { label: 'DECSEL', description: 'Erase in Line' }, L: { label: 'IL', description: 'Insert Ps Line(s)' }, M: { label: 'DL', description: 'Delete Ps Line(s)' }, P: { label: 'DCH', description: 'Delete Ps Character(s)' }, ' q': { label: 'DECSCUSR', description: 'Set Cursor Style', paramCount: 1 } }; for (const s of Object.keys(buttonSpecs)) { const spec = buttonSpecs[s]; vtFragment.appendChild(createButton(spec.label, spec.description, s, spec.paramCount)); } document.querySelector('#vt-container').appendChild(vtFragment); } function ligaturesTest(): void { term.write([ '', '-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-', '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=', '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __', '<~~ /> ~~> == != /= ~= <> === !== !=== =/= =!=', '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>', '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -|