658 lines
26 KiB
TypeScript
658 lines
26 KiB
TypeScript
/**
|
|
* 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 } 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 ITheme } from '@xterm/xterm';
|
|
import { AttachAddon } from '@xterm/addon-attach';
|
|
import { AddonImageWindow } from './components/window/addonImageWindow';
|
|
import { AddonLigaturesWindow } from './components/window/addonLigaturesWindow';
|
|
import { AddonProgressWindow } from './components/window/addonProgressWindow';
|
|
import { AddonSearchWindow } from './components/window/addonSearchWindow';
|
|
import { AddonSerializeWindow } from './components/window/addonSerializeWindow';
|
|
import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow';
|
|
import { AddonWebLinksWindow } from './components/window/addonWebLinksWindow';
|
|
import { AddonsWindow } from './components/window/addonsWindow';
|
|
import { CellInspectorWindow } from './components/window/cellInspectorWindow';
|
|
import { ControlBar } from './components/controlBar';
|
|
import { WebglWindow } from './components/window/webglWindow';
|
|
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 } from '@xterm/addon-progress';
|
|
import { SearchAddon, ISearchOptions } from '@xterm/addon-search';
|
|
import { SerializeAddon } from '@xterm/addon-serialize';
|
|
import { WebFontsAddon } from '@xterm/addon-web-fonts';
|
|
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;
|
|
ClipboardAddon?: typeof ClipboardAddon;
|
|
FitAddon?: typeof FitAddon;
|
|
ImageAddon?: typeof ImageAddon;
|
|
ProgressAddon?: typeof ProgressAddon;
|
|
SearchAddon?: typeof SearchAddon;
|
|
SerializeAddon?: typeof SerializeAddon;
|
|
WebLinksAddon?: typeof WebLinksAddon;
|
|
WebglAddon?: typeof WebglAddon;
|
|
Unicode11Addon?: typeof Unicode11Addon;
|
|
UnicodeGraphemesAddon?: typeof UnicodeGraphemesAddon;
|
|
LigaturesAddon?: typeof LigaturesAddon;
|
|
}
|
|
declare let window: IWindowWithTerminal;
|
|
|
|
let term: Terminal | null;
|
|
let protocol: string;
|
|
let socketURL: string;
|
|
let socket: WebSocket | null;
|
|
let pid: string;
|
|
let controlBar: ControlBar;
|
|
let addonsWindow: AddonsWindow;
|
|
let addonSearchWindow: AddonSearchWindow;
|
|
let addonWebglWindow: WebglWindow;
|
|
let optionsWindow: OptionsWindow;
|
|
|
|
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 },
|
|
webFonts: { name: 'webFonts', ctor: WebFontsAddon, 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 as any).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')!, []);
|
|
optionsWindow = controlBar.registerWindow(new OptionsWindow(typedTerm, addons, { updateTerminalSize, updateTerminalContainerBackground }));
|
|
const styleWindow = controlBar.registerWindow(new StyleWindow(typedTerm, addons));
|
|
controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons));
|
|
controlBar.registerWindow(new VtWindow(typedTerm, addons));
|
|
addonsWindow = controlBar.registerWindow(new AddonsWindow(typedTerm, addons));
|
|
controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
|
|
controlBar.registerWindow(new AddonLigaturesWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
|
|
controlBar.registerWindow(new AddonProgressWindow(typedTerm, addons), { afterId: 'addon-ligatures', hidden: true, italics: true });
|
|
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addon-progress', hidden: true, italics: true });
|
|
controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true });
|
|
controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
|
|
controlBar.registerWindow(new AddonWebLinksWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
|
|
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-links', hidden: true, italics: true });
|
|
controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
|
|
actionElements = {
|
|
findNext: addonSearchWindow.findNextInput,
|
|
findPrevious: addonSearchWindow.findPreviousInput,
|
|
findResults: addonSearchWindow.findResultsSpan
|
|
};
|
|
controlBar.activateDefaultTab();
|
|
|
|
// TODO: Most of below should be encapsulated within windows
|
|
paddingElement = styleWindow.paddingElement;
|
|
|
|
controlBar.setTabVisible('addon-image', !!addons.image.instance);
|
|
controlBar.setTabVisible('addon-ligatures', !!addons.ligatures.instance);
|
|
controlBar.setTabVisible('addon-progress', !!addons.progress.instance);
|
|
controlBar.setTabVisible('addon-search', true);
|
|
controlBar.setTabVisible('addon-serialize', true);
|
|
controlBar.setTabVisible('addon-web-fonts', true);
|
|
controlBar.setTabVisible('addon-web-links', !!addons.webLinks.instance);
|
|
controlBar.setTabVisible('addon-webgl', true);
|
|
addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
|
|
addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
|
addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
|
addons.webgl.instance!.onRemoveTextureAtlasCanvas(e => addonWebglWindow.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();
|
|
});
|
|
}
|
|
|
|
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();
|
|
addons.webFonts.instance = new WebFontsAddon();
|
|
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.webFonts.instance);
|
|
typedTerm.loadAddon(addons.clipboard.instance);
|
|
|
|
(window as any).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 pixelWidth = Math.round(term!.dimensions?.css?.canvas?.width ?? 0);
|
|
const pixelHeight = Math.round(term!.dimensions?.css?.canvas?.height ?? 0);
|
|
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows + '&pixelWidth=' + pixelWidth + '&pixelHeight=' + pixelHeight;
|
|
|
|
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) {
|
|
// In general this should be debounced to avoid excessive work on the main
|
|
// thread by firing the expensive resize action repeatedly
|
|
addons.fit.instance!.fit();
|
|
}
|
|
});
|
|
resizeObserver.observe(terminalContainer!);
|
|
|
|
window.addEventListener('resize', () => {
|
|
terminalContainer!.style.width = document.body.clientWidth + 'px';
|
|
});
|
|
|
|
// fit is called within a setTimeout, cols and rows need this.
|
|
setTimeout(async () => {
|
|
optionsWindow.initOptions(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 pixelWidth = Math.round(term!.dimensions?.css?.canvas?.width ?? 0);
|
|
const pixelHeight = Math.round(term!.dimensions?.css?.canvas?.height ?? 0);
|
|
const res = await fetch('/terminals?cols=' + term!.cols + '&rows=' + term!.rows + '&pixelWidth=' + pixelWidth + '&pixelHeight=' + pixelHeight, { 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 as any)._initialized = true;
|
|
initAddons(term!);
|
|
}
|
|
|
|
function runFakeTerminal(): void {
|
|
if ((term as any)._initialized) {
|
|
return;
|
|
}
|
|
|
|
(term as any)._initialized = true;
|
|
initAddons(term!);
|
|
|
|
(term as any).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 as any).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 as any).prompt();
|
|
} else if (ev.keyCode === 8) {
|
|
// Do not delete the prompt
|
|
if ((term as any)._core.buffer.x > 2) {
|
|
term!.write('\b \b');
|
|
}
|
|
} else if (printable) {
|
|
term!.write(e.key);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateTerminalContainerBackground(): void {
|
|
if (term!.options.allowTransparency) {
|
|
terminalContainer!.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px';
|
|
} else {
|
|
terminalContainer!.style.background = term!.options.theme?.background ?? '#000000';
|
|
}
|
|
}
|
|
|
|
function initAddons(term: Terminal): void {
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
function postInitWebgl(): void {
|
|
controlBar.setTabVisible('addon-webgl', true);
|
|
setTimeout(() => {
|
|
addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
|
|
addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
|
addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
|
}, 500);
|
|
}
|
|
function preDisposeWebgl(): void {
|
|
controlBar.setTabVisible('addon-webgl', false);
|
|
if (addons.webgl.instance!.textureAtlas) {
|
|
addons.webgl.instance!.textureAtlas.remove();
|
|
}
|
|
}
|
|
|
|
(Object.keys(addons) as AddonType[]).forEach(name => {
|
|
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);
|
|
controlBar.setTabVisible('addon-image', true);
|
|
} else {
|
|
controlBar.setTabVisible('addon-image', false);
|
|
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') {
|
|
controlBar.setTabVisible('addon-search', true);
|
|
addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
|
|
} else if (name === 'serialize') {
|
|
controlBar.setTabVisible('addon-serialize', true);
|
|
} else if (name === 'ligatures') {
|
|
controlBar.setTabVisible('addon-ligatures', true);
|
|
} else if (name === 'progress') {
|
|
controlBar.setTabVisible('addon-progress', true);
|
|
} else if (name === 'webLinks') {
|
|
controlBar.setTabVisible('addon-web-links', true);
|
|
}
|
|
}
|
|
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';
|
|
} else if (name === 'search') {
|
|
controlBar.setTabVisible('addon-search', false);
|
|
} else if (name === 'serialize') {
|
|
controlBar.setTabVisible('addon-serialize', false);
|
|
} else if (name === 'ligatures') {
|
|
controlBar.setTabVisible('addon-ligatures', false);
|
|
} else if (name === 'progress') {
|
|
controlBar.setTabVisible('addon-progress', false);
|
|
} else if (name === 'webLinks') {
|
|
controlBar.setTabVisible('addon-web-links', false);
|
|
}
|
|
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 as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
|
}
|
|
|
|
function updateTerminalSize(): void {
|
|
const width = optionsWindow.autoResize ? '100%'
|
|
: ((term as any).dimensions.css.canvas.width + (term as any)._core.viewport.scrollBarWidth).toString() + 'px';
|
|
const height = optionsWindow.autoResize ? '100%'
|
|
: ((term as any).dimensions.css.canvas.height).toString() + 'px';
|
|
terminalContainer!.style.width = width;
|
|
terminalContainer!.style.height = height;
|
|
addons.fit.instance!.fit();
|
|
}
|
|
|
|
(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();
|
|
};
|