758 lines
30 KiB
TypeScript
758 lines
30 KiB
TypeScript
/**
|
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*
|
|
* This file is the entry point for browserify.
|
|
*/
|
|
|
|
/// <reference path="../typings/xterm.d.ts"/>
|
|
|
|
/* 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<HTMLTextAreaElement>('#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<Exclude<AddonType, 'attach'>>).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<HTMLInputElement>('#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<HTMLInputElement>('#image-showplaceholder');
|
|
showPlaceholderElement.checked = addons.image.instance.showPlaceholder;
|
|
addDomListener(showPlaceholderElement, 'change', () => {
|
|
addons.image.instance.showPlaceholder = showPlaceholderElement.checked;
|
|
});
|
|
const ctorOptionsElement = document.querySelector<HTMLTextAreaElement>('#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\\'));
|
|
}
|