Merge pull request #5498 from Tyriar/demo

Improve demo
This commit is contained in:
Daniel Imms
2025-12-26 03:29:03 -08:00
committed by GitHub
10 changed files with 1251 additions and 488 deletions
+60 -269
View File
@@ -19,6 +19,13 @@ if ('WebAssembly' in window) {
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';
@@ -55,7 +62,13 @@ let protocol;
let socketURL;
let socket;
let pid;
let autoResize: boolean = true;
let controlBar: ControlBar;
let addonsWindow: AddonsWindow;
let gpuWindow: GpuWindow;
let optionsWindow: OptionsWindow;
let styleWindow: StyleWindow;
let testWindow: TestWindow;
let vtWindow: VtWindow;
type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures';
@@ -110,13 +123,12 @@ const addons: { [T in AddonType]: IDemoAddon<T> } = {
};
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')
let actionElements: {
findNext: HTMLInputElement;
findPrevious: HTMLInputElement;
findResults: HTMLElement;
};
const paddingElement = document.getElementById('padding') as HTMLInputElement;
let paddingElement: HTMLInputElement;
const xtermjsTheme = {
foreground: '#F8F8F8',
@@ -230,6 +242,26 @@ if (document.location.pathname === '/test') {
window.WebLinksAddon = WebLinksAddon;
window.WebglAddon = WebglAddon;
} else {
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);
controlBar.registerWindow(optionsWindow);
styleWindow = new StyleWindow();
controlBar.registerWindow(styleWindow);
paddingElement = styleWindow.paddingElement;
testWindow = new TestWindow();
controlBar.registerWindow(testWindow);
vtWindow = new VtWindow();
controlBar.registerWindow(vtWindow);
controlBar.activateDefaultTab();
createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('create-new-window').addEventListener('click', createNewWindowButtonHandler);
@@ -253,7 +285,6 @@ if (document.location.pathname === '/test') {
document.getElementById('ligatures-test').addEventListener('click', ligaturesTest);
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
document.getElementById('bce').addEventListener('click', coloredErase);
addVtButtons();
initImageAddonExposed();
testEvents();
progressButtons();
@@ -321,10 +352,11 @@ function createTerminal(): void {
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));
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));
} catch (e) {
console.warn('error during loading webgl addon:', e);
addons.webgl.instance.dispose();
@@ -337,9 +369,11 @@ function createTerminal(): void {
}
term.focus();
updateTerminalContainerBackground();
vtWindow?.initTerminal(term);
const resizeObserver = new ResizeObserver(entries => {
if (autoResize) {
if (optionsWindow.autoResize) {
addons.fit.instance.fit();
}
});
@@ -374,7 +408,7 @@ function createTerminal(): void {
// fit is called within a setTimeout, cols and rows need this.
setTimeout(async () => {
initOptions(term);
optionsWindow.initOptions(term, addDomListener);
paddingElement.value = '0';
// Set terminal size again to set the specific dimensions on the demo
@@ -438,194 +472,24 @@ function runFakeTerminal(): void {
});
}
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',
];
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 += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
html += '</div><div class="option-group">';
numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.options[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
});
html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => {
if (o === 'colsRows') {
html += `<div class="option"><label>size (<var>cols</var><code>x</code><var>rows</var> or <code>auto</code>) <input id="opt-${o}" type="text" value="auto"/></label></div>`;
} else if (stringOptions[o]) {
const selectedOption = o === 'theme' ? 'xtermjs' : term.options[o];
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${v === selectedOption ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.options[o]}"/></label></div>`;
}
});
html += '</div>';
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 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(() => {
setTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
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();
}
@@ -741,7 +605,7 @@ function initAddons(term: Terminal): void {
fragment.appendChild(wrapper);
});
const container = document.getElementById('addons-container');
const container = addonsWindow.addonsContainer;
container.innerHTML = '';
container.appendChild(fragment);
}
@@ -762,9 +626,9 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
}
function updateTerminalSize(): void {
const width = autoResize ? '100%'
const width = optionsWindow.autoResize ? '100%'
: (term._core._renderService.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = autoResize ? '100%'
const height = optionsWindow.autoResize ? '100%'
: (term._core._renderService.dimensions.css.canvas.height).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
@@ -797,21 +661,7 @@ function htmlSerializeButtonHandler(): void {
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 customGlyphAlignmentHandler(): void {
term.write('\n\r');
@@ -1445,65 +1295,6 @@ function decorationStressTest(): void {
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' },
'?2026h': { label: 'BSU', description: 'Begin synchronized update', paramCount: 0 },
'?2026l': { label: 'ESU', description: 'End synchronized update', paramCount: 0 }
};
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([
'',
+230
View File
@@ -0,0 +1,230 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
export interface ITabConfig {
id: string;
label: string;
}
export interface IControlWindow {
readonly id: string;
readonly label: string;
build(container: HTMLElement): void;
}
export class ControlBar {
private readonly _sidebar: HTMLElement;
private readonly _resizeHandleH: HTMLElement;
private readonly _resizeHandleV: HTMLElement;
private readonly _resizeHandleCorner: HTMLElement;
private _resizeMode: 'none' | 'horizontal' | 'vertical' | 'corner' = 'none';
private readonly _tabContainer: HTMLElement;
private readonly _tabs: Map<string, { button: HTMLButtonElement; content: HTMLElement }> = new Map();
private _activeTabId: string | null = null;
constructor(sidebar: HTMLElement, tabContainer: HTMLElement, tabs: ITabConfig[]) {
this._sidebar = sidebar;
this._tabContainer = tabContainer;
// Create resize handles
this._resizeHandleH = document.createElement('div');
this._resizeHandleH.id = 'sidebar-resize-handle-horizontal';
this._resizeHandleV = document.createElement('div');
this._resizeHandleV.id = 'sidebar-resize-handle-vertical';
this._resizeHandleCorner = document.createElement('div');
this._resizeHandleCorner.id = 'sidebar-resize-handle-corner';
// Insert handles at the beginning of the sidebar
this._sidebar.prepend(this._resizeHandleCorner);
this._sidebar.prepend(this._resizeHandleV);
this._sidebar.prepend(this._resizeHandleH);
this._initResizeListeners();
this._initTabs(tabs);
}
private _initResizeListeners(): void {
this._resizeHandleH.addEventListener('mousedown', (e: MouseEvent) => this._startResize('horizontal', e));
this._resizeHandleV.addEventListener('mousedown', (e: MouseEvent) => this._startResize('vertical', e));
this._resizeHandleCorner.addEventListener('mousedown', (e: MouseEvent) => this._startResize('corner', e));
document.addEventListener('mousemove', (e: MouseEvent) => {
if (this._resizeMode === 'none') return;
if (this._resizeMode === 'horizontal' || this._resizeMode === 'corner') {
const newWidth = window.innerWidth - e.clientX - 10;
this._sidebar.style.width = `${Math.max(200, newWidth)}px`;
}
if (this._resizeMode === 'vertical' || this._resizeMode === 'corner') {
const rect = this._sidebar.getBoundingClientRect();
const newHeight = e.clientY - rect.top;
this._sidebar.style.height = `${Math.max(100, newHeight)}px`;
}
});
document.addEventListener('mouseup', () => {
this._resizeMode = 'none';
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
}
private _startResize(mode: 'horizontal' | 'vertical' | 'corner', e: MouseEvent): void {
this._resizeMode = mode;
document.body.style.cursor = mode === 'horizontal' ? 'ew-resize' : mode === 'vertical' ? 'ns-resize' : 'nesw-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
}
private _initTabs(tabs: ITabConfig[]): void {
// Clear existing buttons
this._tabContainer.innerHTML = '';
// Create tab buttons
for (const tab of tabs) {
const content = document.getElementById(tab.id);
if (!content) {
console.warn(`Tab content element not found: ${tab.id}`);
continue;
}
const button = document.createElement('button');
button.id = `${tab.id}button`;
button.className = 'tabLinks';
button.textContent = tab.label;
button.addEventListener('click', (e) => this._openSection(e, tab.id));
this._tabContainer.appendChild(button);
this._tabs.set(tab.id, { button, content });
// Hide content initially
content.style.display = 'none';
}
// Restore saved tab or default to first
const savedTab = localStorage.getItem('tab');
const tabId = savedTab && this._tabs.has(savedTab) ? savedTab : tabs[0]?.id;
if (tabId) {
this._activateTab(tabId);
}
}
private _openSection(event: MouseEvent, tabId: string): void {
const tab = this._tabs.get(tabId);
if (!tab) return;
// If clicking active tab, toggle sidebar visibility
if (tab.button.classList.contains('active')) {
this._sidebar.classList.toggle('sidebar-hidden');
return;
}
// Show sidebar if hidden
this._sidebar.classList.remove('sidebar-hidden');
this._activateTab(tabId);
}
private _activateTab(tabId: string): void {
// Deactivate all tabs
for (const [, { button, content }] of this._tabs) {
button.classList.remove('active');
content.style.display = 'none';
}
// Activate the selected tab
const tab = this._tabs.get(tabId);
if (tab) {
tab.button.classList.add('active');
tab.content.style.display = 'block';
this._activeTabId = tabId;
localStorage.setItem('tab', tabId);
}
}
public registerWindow(window: IControlWindow, options?: { afterId?: string; hidden?: boolean; smallTab?: boolean }): void {
// Create button
const button = document.createElement('button');
button.id = `${window.id}button`;
button.className = 'tabLinks';
button.textContent = window.label;
button.addEventListener('click', (e) => this._openSection(e, window.id));
// Apply small tab styling
if (options?.smallTab) {
button.style.fontSize = '0.85em';
}
// Insert after specified tab or append at end
if (options?.afterId) {
const afterTab = this._tabs.get(options.afterId);
if (afterTab?.button.nextSibling) {
this._tabContainer.insertBefore(button, afterTab.button.nextSibling);
} else {
this._tabContainer.appendChild(button);
}
} else {
this._tabContainer.appendChild(button);
}
// Hide if specified
if (options?.hidden) {
button.style.display = 'none';
}
// Create content container
const content = document.createElement('div');
content.id = window.id;
content.className = 'tabContent';
content.style.display = 'none';
this._sidebar.appendChild(content);
// Let the window build its content
window.build(content);
this._tabs.set(window.id, { button, content });
}
public setTabVisible(tabId: string, visible: boolean): void {
const tab = this._tabs.get(tabId);
if (tab) {
tab.button.style.display = visible ? '' : 'none';
// If hiding the active tab, switch to first visible tab
if (!visible && this._activeTabId === tabId) {
for (const [id, t] of this._tabs) {
if (t.button.style.display !== 'none') {
this._activateTab(id);
break;
}
}
}
}
}
public get activeTabId(): string | null {
return this._activeTabId;
}
public activateDefaultTab(): void {
// Restore saved tab or default to first visible tab
const savedTab = localStorage.getItem('tab');
if (savedTab && this._tabs.has(savedTab)) {
const tab = this._tabs.get(savedTab);
if (tab && tab.button.style.display !== 'none') {
this._activateTab(savedTab);
return;
}
}
// Fall back to first visible tab
for (const [id, tab] of this._tabs) {
if (tab.button.style.display !== 'none') {
this._activateTab(id);
return;
}
}
}
}
+246
View File
@@ -0,0 +1,246 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { Terminal } from '@xterm/xterm';
import type { IControlWindow } from '../controlBar';
export class AddonsWindow implements IControlWindow {
public readonly id = 'addons';
public readonly label = 'Addons';
private _addonsContainer: HTMLElement;
private _findNextInput: HTMLInputElement;
private _findPreviousInput: HTMLInputElement;
private _findResultsSpan: HTMLElement;
private _regexCheckbox: HTMLInputElement;
private _caseSensitiveCheckbox: HTMLInputElement;
private _wholeWordCheckbox: HTMLInputElement;
private _highlightAllMatchesCheckbox: HTMLInputElement;
private _serializeOutputPre: HTMLPreElement;
private _htmlSerializeOutputPre: HTMLPreElement;
private _htmlSerializeOutputResult: HTMLElement;
private _writeToTerminalCheckbox: HTMLInputElement;
private _imageStorageLimitInput: HTMLInputElement;
private _imageShowPlaceholderCheckbox: HTMLInputElement;
private _imageOptionsTextarea: HTMLTextAreaElement;
public build(container: HTMLElement): void {
// Heading
const heading = document.createElement('h3');
heading.textContent = 'Addons';
container.appendChild(heading);
// Description
const description = document.createElement('p');
description.textContent = 'Addons can be loaded and unloaded on a particular terminal to extend its functionality.';
container.appendChild(description);
// Addons container (checkboxes go here)
this._addonsContainer = document.createElement('div');
this._addonsContainer.id = 'addons-container';
container.appendChild(this._addonsContainer);
// Addons Control section
const controlHeading = document.createElement('h3');
controlHeading.textContent = 'Addons Control';
container.appendChild(controlHeading);
// SearchAddon section
this._buildSearchSection(container);
// SerializeAddon section
this._buildSerializeSection(container);
// ImageAddon section
this._buildImageSection(container);
}
private _buildSearchSection(container: HTMLElement): void {
const h4 = document.createElement('h4');
h4.textContent = 'SearchAddon';
container.appendChild(h4);
const wrapper = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'column';
// Find next
const findNextLabel = document.createElement('label');
findNextLabel.textContent = 'Find next ';
this._findNextInput = document.createElement('input');
this._findNextInput.id = 'find-next';
findNextLabel.appendChild(this._findNextInput);
wrapper.appendChild(findNextLabel);
// Find previous
const findPrevLabel = document.createElement('label');
findPrevLabel.textContent = 'Find previous ';
this._findPreviousInput = document.createElement('input');
this._findPreviousInput.id = 'find-previous';
findPrevLabel.appendChild(this._findPreviousInput);
wrapper.appendChild(findPrevLabel);
// Results
const resultsDiv = document.createElement('div');
resultsDiv.textContent = 'Results: ';
this._findResultsSpan = document.createElement('span');
this._findResultsSpan.id = 'find-results';
resultsDiv.appendChild(this._findResultsSpan);
wrapper.appendChild(resultsDiv);
// Regex checkbox
const regexLabel = document.createElement('label');
this._regexCheckbox = document.createElement('input');
this._regexCheckbox.type = 'checkbox';
this._regexCheckbox.id = 'regex';
regexLabel.appendChild(this._regexCheckbox);
regexLabel.appendChild(document.createTextNode('Use regex'));
wrapper.appendChild(regexLabel);
// Case sensitive checkbox
const caseLabel = document.createElement('label');
this._caseSensitiveCheckbox = document.createElement('input');
this._caseSensitiveCheckbox.type = 'checkbox';
this._caseSensitiveCheckbox.id = 'case-sensitive';
caseLabel.appendChild(this._caseSensitiveCheckbox);
caseLabel.appendChild(document.createTextNode('Case sensitive'));
wrapper.appendChild(caseLabel);
// Whole word checkbox
const wholeWordLabel = document.createElement('label');
this._wholeWordCheckbox = document.createElement('input');
this._wholeWordCheckbox.type = 'checkbox';
this._wholeWordCheckbox.id = 'whole-word';
wholeWordLabel.appendChild(this._wholeWordCheckbox);
wholeWordLabel.appendChild(document.createTextNode('Whole word'));
wrapper.appendChild(wholeWordLabel);
// Highlight all matches checkbox
const highlightLabel = document.createElement('label');
this._highlightAllMatchesCheckbox = document.createElement('input');
this._highlightAllMatchesCheckbox.type = 'checkbox';
this._highlightAllMatchesCheckbox.id = 'highlight-all-matches';
this._highlightAllMatchesCheckbox.checked = true;
highlightLabel.appendChild(this._highlightAllMatchesCheckbox);
highlightLabel.appendChild(document.createTextNode('Highlight All Matches'));
wrapper.appendChild(highlightLabel);
container.appendChild(wrapper);
}
private _buildSerializeSection(container: HTMLElement): void {
const h4 = document.createElement('h4');
h4.textContent = 'SerializeAddon';
container.appendChild(h4);
const wrapper = document.createElement('div');
// Serialize button
const serializeBtn = document.createElement('button');
serializeBtn.id = 'serialize';
serializeBtn.textContent = 'Serialize the content of terminal';
wrapper.appendChild(serializeBtn);
// Write to terminal checkbox
const writeLabel = document.createElement('label');
this._writeToTerminalCheckbox = document.createElement('input');
this._writeToTerminalCheckbox.type = 'checkbox';
this._writeToTerminalCheckbox.id = 'write-to-terminal';
writeLabel.appendChild(this._writeToTerminalCheckbox);
writeLabel.appendChild(document.createTextNode('Write back to terminal'));
wrapper.appendChild(writeLabel);
// Serialize output
const outputDiv = document.createElement('div');
this._serializeOutputPre = document.createElement('pre');
this._serializeOutputPre.id = 'serialize-output';
outputDiv.appendChild(this._serializeOutputPre);
wrapper.appendChild(outputDiv);
// HTML serialize button
const htmlSerializeBtn = document.createElement('button');
htmlSerializeBtn.id = 'htmlserialize';
htmlSerializeBtn.textContent = 'Serialize the content of terminal in HTML';
wrapper.appendChild(htmlSerializeBtn);
// HTML serialize result
this._htmlSerializeOutputResult = document.createElement('span');
this._htmlSerializeOutputResult.id = 'htmlserialize-output-result';
wrapper.appendChild(this._htmlSerializeOutputResult);
// HTML serialize output
const htmlOutputDiv = document.createElement('div');
this._htmlSerializeOutputPre = document.createElement('pre');
this._htmlSerializeOutputPre.id = 'htmlserialize-output';
htmlOutputDiv.appendChild(this._htmlSerializeOutputPre);
wrapper.appendChild(htmlOutputDiv);
container.appendChild(wrapper);
}
private _buildImageSection(container: HTMLElement): void {
const h4 = document.createElement('h4');
h4.textContent = 'Image Addon';
container.appendChild(h4);
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = 'image addon settings';
details.appendChild(summary);
const wrapper = document.createElement('div');
// Storage limit
const storageLimitLabel = document.createElement('label');
storageLimitLabel.textContent = 'Storage Limit (in MB) ';
this._imageStorageLimitInput = document.createElement('input');
this._imageStorageLimitInput.type = 'number';
this._imageStorageLimitInput.id = 'image-storagelimit';
storageLimitLabel.appendChild(this._imageStorageLimitInput);
wrapper.appendChild(storageLimitLabel);
wrapper.appendChild(document.createElement('br'));
// Show placeholder
const placeholderLabel = document.createElement('label');
placeholderLabel.textContent = 'Show Placeholder ';
this._imageShowPlaceholderCheckbox = document.createElement('input');
this._imageShowPlaceholderCheckbox.type = 'checkbox';
this._imageShowPlaceholderCheckbox.id = 'image-showplaceholder';
placeholderLabel.appendChild(this._imageShowPlaceholderCheckbox);
wrapper.appendChild(placeholderLabel);
wrapper.appendChild(document.createElement('br'));
wrapper.appendChild(document.createElement('br'));
// Ctor options
const optionsLabel = document.createElement('label');
optionsLabel.appendChild(document.createTextNode('Ctor options (applied on addon relaunch)'));
optionsLabel.appendChild(document.createElement('br'));
this._imageOptionsTextarea = document.createElement('textarea');
this._imageOptionsTextarea.id = 'image-options';
this._imageOptionsTextarea.cols = 40;
this._imageOptionsTextarea.rows = 12;
optionsLabel.appendChild(this._imageOptionsTextarea);
wrapper.appendChild(optionsLabel);
details.appendChild(wrapper);
container.appendChild(details);
}
public get addonsContainer(): HTMLElement {
return this._addonsContainer;
}
public get findNextInput(): HTMLInputElement {
return this._findNextInput;
}
public get findPreviousInput(): HTMLInputElement {
return this._findPreviousInput;
}
public get findResultsSpan(): HTMLElement {
return this._findResultsSpan;
}
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IControlWindow } from '../controlBar';
export class GpuWindow implements IControlWindow {
public readonly id = 'gpu';
public readonly label = 'GPU';
private _textureAtlasContainer: HTMLElement;
public build(container: HTMLElement): void {
const heading = document.createElement('h3');
heading.textContent = 'GPU';
container.appendChild(heading);
const zoomCheckbox = document.createElement('input');
zoomCheckbox.type = 'checkbox';
zoomCheckbox.id = 'texture-atlas-zoom';
container.appendChild(zoomCheckbox);
const zoomLabel = document.createElement('label');
zoomLabel.htmlFor = 'texture-atlas-zoom';
zoomLabel.textContent = 'Zoom texture atlas';
container.appendChild(zoomLabel);
this._textureAtlasContainer = document.createElement('div');
this._textureAtlasContainer.id = 'texture-atlas';
container.appendChild(this._textureAtlasContainer);
}
public setTextureAtlas(canvas: HTMLCanvasElement): void {
this._styleAtlasPage(canvas);
this._textureAtlasContainer.replaceChildren(canvas);
}
public appendTextureAtlas(canvas: HTMLCanvasElement): void {
this._styleAtlasPage(canvas);
this._textureAtlasContainer.appendChild(canvas);
}
public removeTextureAtlas(canvas: HTMLCanvasElement): void {
canvas.remove();
}
private _styleAtlasPage(canvas: HTMLCanvasElement): void {
canvas.style.width = `${canvas.width / window.devicePixelRatio}px`;
canvas.style.height = `${canvas.height / window.devicePixelRatio}px`;
}
}
+250
View File
@@ -0,0 +1,250 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { Terminal, ITheme } from '@xterm/xterm';
import type { IControlWindow } from '../controlBar';
const xtermjsTheme: ITheme = {
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'
};
export class OptionsWindow implements IControlWindow {
public readonly id = 'options';
public readonly label = 'Options';
private _container: HTMLElement;
private _optionsContainer: HTMLElement;
private _term: Terminal;
private _autoResize: boolean = true;
private _updateTerminalSize: () => void;
private _updateTerminalContainerBackground: () => void;
constructor(
updateTerminalSize: () => void,
updateTerminalContainerBackground: () => void
) {
this._updateTerminalSize = updateTerminalSize;
this._updateTerminalContainerBackground = updateTerminalContainerBackground;
}
public build(container: HTMLElement): void {
this._container = container;
const heading = document.createElement('h3');
heading.textContent = 'Options';
container.appendChild(heading);
const description = document.createElement('p');
description.innerHTML = 'These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.options</code> property.';
container.appendChild(description);
this._optionsContainer = document.createElement('div');
this._optionsContainer.id = 'options-container';
container.appendChild(this._optionsContainer);
}
public initOptions(term: Terminal, addDomListener: (el: HTMLElement, type: string, handler: (...args: any[]) => any) => void): void {
this._term = term;
const blacklistedOptions = [
'cancelEvents',
'convertEol',
'termName',
'cols', 'rows',
'documentOverride',
'linkHandler',
'logger',
'overviewRuler',
'theme',
'windowOptions',
'windowsPty',
];
const stringOptions: { [key: string]: string[] | null } = {
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: string[] = [];
const numberOptions: string[] = [];
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 += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
html += '</div><div class="option-group">';
numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.options[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
});
html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => {
if (o === 'colsRows') {
html += `<div class="option"><label>size (<var>cols</var><code>x</code><var>rows</var> or <code>auto</code>) <input id="opt-${o}" type="text" value="auto"/></label></div>`;
} else if (stringOptions[o]) {
const selectedOption = o === 'theme' ? 'xtermjs' : term.options[o];
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o]!.map(v => `<option ${v === selectedOption ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.options[o]}"/></label></div>`;
}
});
html += '</div>';
this._optionsContainer.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(() => this._updateTerminalSize(), 5);
} else {
term.options[o] = parseInt(input.value);
}
this._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) {
this._autoResize = false;
term.resize(parseInt(m[1]), parseInt(m[2]));
} else {
this._autoResize = true;
input.value = 'auto';
this._updateTerminalSize();
}
} else if (o === 'theme') {
switch (input.value) {
case 'default':
value = undefined;
break;
case 'xtermjs':
value = xtermjsTheme;
break;
case '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':
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;
if (o === 'theme') {
this._updateTerminalContainerBackground();
}
});
});
}
public get autoResize(): boolean {
return this._autoResize;
}
public set autoResize(value: boolean) {
this._autoResize = value;
}
}
+39
View File
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IControlWindow } from '../controlBar';
export class StyleWindow implements IControlWindow {
public readonly id = 'style';
public readonly label = 'Style';
private _paddingElement: HTMLInputElement;
public build(container: HTMLElement): void {
const heading = document.createElement('h3');
heading.textContent = 'Style';
container.appendChild(heading);
const wrapper = document.createElement('div');
wrapper.style.display = 'inline-block';
wrapper.style.marginRight = '16px';
const label = document.createElement('label');
label.htmlFor = 'padding';
label.textContent = 'Padding';
wrapper.appendChild(label);
this._paddingElement = document.createElement('input');
this._paddingElement.type = 'number';
this._paddingElement.id = 'padding';
wrapper.appendChild(this._paddingElement);
container.appendChild(wrapper);
}
public get paddingElement(): HTMLInputElement {
return this._paddingElement;
}
}
+177
View File
@@ -0,0 +1,177 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IControlWindow } from '../controlBar';
export class TestWindow implements IControlWindow {
public readonly id = 'test';
public readonly label = 'Test';
public build(container: HTMLElement): void {
const heading = document.createElement('h3');
heading.textContent = 'Test';
container.appendChild(heading);
const wrapper = document.createElement('div');
wrapper.style.display = 'inline-block';
wrapper.style.marginRight = '16px';
const dl = document.createElement('dl');
// Lifecycle section
this._addDt(dl, 'Lifecycle');
this._addDdWithCheckbox(dl, 'use-real-terminal', 'Use real terminal', 'This is used to real vs fake terminals', true);
this._addDdWithButton(dl, 'dispose', 'Dispose terminal', 'This is used to testing memory leaks');
this._addDdWithButton(dl, 'create-new-window', 'Create terminal in new window', 'This is used to test rendering in other windows');
// Performance section
this._addDt(dl, 'Performance');
this._addDdWithButton(dl, 'load-test', 'Load test', 'Write several MB of data to simulate a lot of data coming from the process');
this._addDdWithButton(dl, 'load-test-long-lines', 'Load test (long lines)', 'Write several MB of data with long lines to simulate a lot of data coming from the process');
this._addDdWithButton(dl, 'print-cjk', 'CJK Unified Ideographs', 'Prints the 20977 characters from the CJK Unified Ideographs unicode block');
this._addDdWithButton(dl, 'print-cjk-sgr', 'CJK Unified Ideographs (random SGR)', 'Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized SGR attributes');
// Styles section
this._addDt(dl, 'Styles');
this._addDdWithButton(dl, 'custom-glyph-alignment', 'Custom glyph alignment test', 'Write custom glyph alignment tests to the terminal');
this._addDdWithButton(dl, 'custom-glyph-ranges', 'Custom glyph ranges', 'Write custom glyph unicode range to the terminal');
this._addDdWithButton(dl, 'powerline-symbol-test', 'Powerline symbol test', 'Write powerline symbol characters to the terminal (\\ue0a0+)');
this._addDdWithButton(dl, 'underline-test', 'Underline test', 'Write text with Kitty\'s extended underline sequences');
this._addDdWithButton(dl, 'sgr-test', 'SGR test', 'Write text with SGR attribute');
this._addDdWithButton(dl, 'ansi-colors', 'Ansi colors test', 'Write a wide range of ansi colors');
this._addDdWithButton(dl, 'osc-hyperlinks', 'Ansi hyperlinks test', 'Write some OSC 8 hyperlinks');
this._addDdWithButton(dl, 'bce', 'Colored Erase (BCE)', 'Test colored erase');
this._addDdWithButton(dl, 'add-grapheme-clusters', 'Grapheme clusters', 'Write grapheme cluster test strings');
// Decorations section
this._addDt(dl, 'Decorations');
this._addDdWithButton(dl, 'add-decoration', 'Decoration', 'Add a decoration to the terminal');
this._addDdWithButton(dl, 'add-overview-ruler', 'Add Overview Ruler', 'Add an overview ruler to the terminal');
this._addDdWithButton(dl, 'decoration-stress-test', 'Stress Test', 'Toggle between adding and removing a decoration to each line');
// Ligatures Addon section
this._addDt(dl, 'Ligatures Addon');
this._addDdWithButton(dl, 'ligatures-test', 'Common ligatures', 'Write common ligatures sequences');
// Weblinks Addon section
this._addDt(dl, 'Weblinks Addon');
this._addDdWithButton(dl, 'weblinks-test', 'Test URLs', 'Various url conditions from demo data, hover&click to test');
// Image Test section
this._addDt(dl, 'Image Test');
this._addDdWithButton(dl, 'image-demo1', 'snake (sixel)', '');
this._addDdWithButton(dl, 'image-demo2', 'oranges (sixel)', '');
this._addDdWithButton(dl, 'image-demo3', 'palette (iip)', '');
// Events Test section
this._addDt(dl, 'Events Test');
this._addDdWithButton(dl, 'event-focus', 'focus', '');
this._addDdWithButton(dl, 'event-blur', 'blur', '');
// Progress Addon section
this._addDt(dl, 'Progress Addon');
this._addDdWithButton(dl, 'progress-run', 'full set run', '');
this._addDdWithButton(dl, 'progress-0', 'state 0: remove', '');
this._addDdWithButton(dl, 'progress-1', 'state 1: set 20%', '');
this._addDdWithButton(dl, 'progress-2', 'state 2: error', '');
this._addDdWithButton(dl, 'progress-3', 'state 3: indeterminate', '');
this._addDdWithButton(dl, 'progress-4', 'state 4: pause', '');
// Progress bar
const progressDd = document.createElement('dd');
const progressDiv = document.createElement('div');
progressDiv.id = 'progress-progress';
const progressPercent = document.createElement('div');
progressPercent.id = 'progress-percent';
const progressIndeterminate = document.createElement('div');
progressIndeterminate.id = 'progress-indeterminate';
progressDiv.appendChild(progressPercent);
progressDiv.appendChild(progressIndeterminate);
progressDd.appendChild(progressDiv);
dl.appendChild(progressDd);
// Progress state
const stateDd = document.createElement('dd');
const stateDiv = document.createElement('div');
stateDiv.id = 'progress-state';
stateDiv.textContent = 'State:';
stateDd.appendChild(stateDiv);
dl.appendChild(stateDd);
wrapper.appendChild(dl);
container.appendChild(wrapper);
// Add progress bar styles
this._addProgressStyles(container);
}
private _addDt(dl: HTMLElement, text: string): void {
const dt = document.createElement('dt');
dt.textContent = text;
dl.appendChild(dt);
}
private _addDdWithButton(dl: HTMLElement, id: string, label: string, title: string): void {
const dd = document.createElement('dd');
const button = document.createElement('button');
button.id = id;
button.textContent = label;
if (title) {
button.title = title;
}
dd.appendChild(button);
dl.appendChild(dd);
}
private _addDdWithCheckbox(dl: HTMLElement, id: string, label: string, title: string, checked: boolean): void {
const dd = document.createElement('dd');
const labelElement = document.createElement('label');
labelElement.htmlFor = id;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = id;
checkbox.checked = checked;
if (title) {
checkbox.title = title;
}
labelElement.appendChild(checkbox);
labelElement.appendChild(document.createTextNode(label));
dd.appendChild(labelElement);
dl.appendChild(dd);
}
private _addProgressStyles(container: HTMLElement): void {
const style = document.createElement('style');
style.textContent = `
#progress-progress {
border: 1px solid black;
height: 10px;
}
#progress-percent {
height: 100%;
}
#progress-indeterminate {
display: none;
position: relative;
height: 100%;
}
#progress-indeterminate:before {
content: '';
position: absolute;
left: 0;
bottom: 0px;
width: 50px;
height: 10px;
background: blue;
animation: ballbns 1s ease-in-out infinite alternate;
}
@keyframes ballbns {
0% { left: 0; transform: translateX(0%); }
100% { left: 100%; transform: translateX(-100%); }
}
`;
container.appendChild(style);
}
}
+95
View File
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IControlWindow } from '../controlBar';
import type { Terminal } from '@xterm/xterm';
export class VtWindow implements IControlWindow {
public readonly id = 'vt';
public readonly label = 'VT';
private _container: HTMLElement;
private _term: Terminal | undefined;
public build(container: HTMLElement): void {
this._container = container;
const heading = document.createElement('h3');
heading.textContent = 'VT';
container.appendChild(heading);
const vtContainer = document.createElement('div');
vtContainer.id = 'vt-container';
container.appendChild(vtContainer);
}
public initTerminal(term: Terminal): void {
this._term = term;
this._addVtButtons();
}
private _addVtButtons(): void {
const vtContainer = this._container.querySelector('#vt-container');
if (!vtContainer) return;
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' },
'?2026h': { label: 'BSU', description: 'Begin synchronized update', paramCount: 0 },
'?2026l': { label: 'ESU', description: 'End synchronized update', paramCount: 0 }
};
for (const s of Object.keys(buttonSpecs)) {
const spec = buttonSpecs[s];
vtFragment.appendChild(this._createButton(spec.label, spec.description, s, spec.paramCount));
}
vtContainer.appendChild(vtFragment);
}
private _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', () => this._term?.write(this._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;
}
private _csi(e: string): string {
return `\x1b[${e}`;
}
}
+5 -190
View File
@@ -12,202 +12,17 @@
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
<div id="banner">
<span class="banner-title">xterm.js</span>
<div class="banner-tabs"></div>
</div>
<div id="container">
<div class="grid">
<div id="terminal-container"></div>
</div>
<div class="grid">
<div class="tab">
<button id="optionsbutton" class="tabLinks" onclick="openSection(event, 'options')">Options</button>
<button id="addonsbutton" class="tabLinks" onclick="openSection(event, 'addons')">Addons</button>
<button id="stylebutton" class="tabLinks" onclick="openSection(event, 'style')">Style</button>
<button id="testbutton" class="tabLinks" onclick="openSection(event, 'test')">Test</button>
<button id="vtbutton" class="tabLinks" onclick="openSection(event, 'vt')">VT</button>
</div>
<div id="options" class="tabContent">
<h3>Options</h3>
<p>These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.options</code> property.</p>
<div id="options-container"></div>
</div>
<div id="addons" class="tabContent">
<h3>Addons</h3>
<p>Addons can be loaded and unloaded on a particular terminal to extend its functionality.</p>
<div id="addons-container"></div>
<h3>Addons Control</h3>
<h4>SearchAddon</h4>
<div style= "display:flex; flex-direction:column;">
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
<div>Results: <span id="find-results"></span></div>
<label><input type="checkbox" id="regex"/>Use regex</label>
<label><input type="checkbox" id="case-sensitive"/>Case sensitive</label>
<label><input type="checkbox" id="whole-word"/>Whole word</label>
<label><input type="checkbox" id="highlight-all-matches" checked/>Highlight All Matches</label>
</div>
<h4>SerializeAddon</h4>
<div>
<button id="serialize">Serialize the content of terminal</button>
<label><input type="checkbox" id="write-to-terminal">Write back to terminal</label>
<div><pre id="serialize-output"></pre></div>
<button id="htmlserialize">Serialize the content of terminal in HTML</button>
<span id="htmlserialize-output-result"></span>
<div><pre id="htmlserialize-output"></pre></div>
</div>
<h4>Image Addon</h4>
<details>
<summary>image addon settings</summary>
<div>
<label>Storage Limit (in MB) <input type="number" id="image-storagelimit"/></label><br/>
<label>Show Placeholder <input type="checkbox" id="image-showplaceholder"/></label>
<br/><br/>
<label>
Ctor options (applied on addon relaunch)<br/>
<textarea id="image-options" cols="40" rows="12"></textarea>
</label>
</div>
</details>
</div>
<div id="style" class="tabContent">
<h3>Style</h3>
<div style="display: inline-block; margin-right: 16px;">
<label for="padding">Padding</label>
<input type="number" id="padding" />
</div>
</div>
<div id="test" class="tabContent">
<h3>Test</h3>
<div style="display: inline-block; margin-right: 16px;">
<dl>
<dt>Lifecycle</dt>
<dd><label for="use-real-terminal"><input type="checkbox" checked id="use-real-terminal" title="This is used to real vs fake terminals" />Use real terminal</label></dd>
<dd><button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button></dd>
<dd><button id="create-new-window" title="This is used to test rendering in other windows">Create terminal in new window</button></dd>
<dt>Performance</dt>
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>
<dd><button id="load-test-long-lines" title="Write several MB of data with long lines to simulate a lot of data coming from the process">Load test (long lines)</button></dd>
<dd><button id="print-cjk" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block">CJK Unified Ideographs</button></dd>
<dd><button id="print-cjk-sgr" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized SGR attributes">CJK Unified Ideographs (random SGR)</button></dd>
<dt>Styles</dt>
<dd><button id="custom-glyph-alignment" title="Write custom glyph alignment tests to the terminal">Custom glyph alignment test</button></dd>
<dd><button id="custom-glyph-ranges" title="Write custom glyph unicode range to the terminal">Custom glyph ranges</button></dd>
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
<dd><button id="bce" title="Test colored erase">Colored Erase (BCE)</button></dd>
<dd><button id="add-grapheme-clusters" title="Write grapheme cluster test strings">Grapheme clusters</button></dd>
<dt>Decorations</dt>
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
<dd><button id="decoration-stress-test" title="Toggle between adding and removing a decoration to each line">Stress Test</button></dd>
<dt>Ligatures Addon</dt>
<dd><button id="ligatures-test" title="Write common ligatures sequences">Common ligatures</button></dd>
<dt>Weblinks Addon</dt>
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>
<dt>Image Test</dt>
<dd><button id="image-demo1">snake (sixel)</button></dd>
<dd><button id="image-demo2">oranges (sixel)</button></dd>
<dd><button id="image-demo3">palette (iip)</button></dd>
<dt>Events Test</dt>
<dd><button id="event-focus">focus</button></dd>
<dd><button id="event-blur">blur</button></dd>
<dt>Progress Addon</dt>
<dd><button id="progress-run">full set run</button></dd>
<dd><button id="progress-0">state 0: remove</button></dd>
<dd><button id="progress-1">state 1: set 20%</button></dd>
<dd><button id="progress-2">state 2: error</button></dd>
<dd><button id="progress-3">state 3: indeterminate</button></dd>
<dd><button id="progress-4">state 4: pause</button></dd>
<style>
#progress-progress {
border: 1px solid black;
height: 10px;
}
#progress-percent {
height: 100%;
}
#progress-indeterminate {
display: none;
position: relative;
height: 100%;
}
#progress-indeterminate:before {
content: '';
position: absolute;
left: 0;
bottom: 0px;
width: 50px;
height: 10px;
background: blue;
animation: ballbns 1s ease-in-out infinite alternate;
}
@keyframes ballbns {
0% { left: 0; transform: translateX(0%); }
100% { left: 100%; transform: translateX(-100%); }
}
</style>
<dd><div id="progress-progress">
<div id="progress-percent"></div>
<div id="progress-indeterminate"></div>
</div></dd>
<dd><div id="progress-state">State:</div></dd>
</dl>
</div>
</div>
<div id="vt" class="tabContent">
<h3>VT</h3>
<div id="vt-container"></div>
</div>
<div id="sidebar" class="grid">
</div>
</div>
<div id="texture-atlas-container">
<input type="checkbox" id="texture-atlas-zoom"/>
<label for="texture-atlas-zoom">Zoom texture atlas</label>
<div id="texture-atlas"></div>
</div>
<script src="dist/client-bundle.js" defer ></script>
<script>
var tab = localStorage.getItem("tab");
if(tab === null){
document.getElementById("options").style.display = "block";
document.getElementById("optionsbutton").classList.add("active");
}
else {
const tabContent = document.getElementsByClassName("tabContent");
let itr;
for (itr = 0; itr < tabContent.length; itr+=1) {
tabContent[itr].style.display = "none";
}
document.getElementById(""+tab+"").style.display = "block";
document.getElementById(""+tab+"button").classList.add("active");
}
function openSection(event, section) {
const tabContent = document.getElementsByClassName("tabContent");
let itr;
for (itr = 0; itr < tabContent.length; itr+=1) {
tabContent[itr].style.display = "none";
}
const tabLinks = document.getElementsByClassName("tabLinks");
for (itr = 0; itr < tabLinks.length; itr+=1) {
tabLinks[itr].className = tabLinks[itr].className.replace(" active", "");
}
document.getElementById(section).style.display = "block";
localStorage.setItem("tab", section);
event.currentTarget.className += " active";
}
</script>
</body>
</html>
+97 -29
View File
@@ -2,16 +2,50 @@ body {
font-family: helvetica, sans-serif, arial;
font-size: 1em;
color: #111;
margin: 0;
padding: 0;
height: 100vh;
display: grid;
grid-template-rows: 24px 1fr;
}
h1 {
text-align: center;
/* Top banner */
#banner {
background: #2D2E2C;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
border-bottom: 1px solid #0005;
}
.banner-title {
color: #5DA5D5;
font-weight: bold;
font-size: 14px;
}
.banner-tabs {
display: flex;
gap: 2px;
}
.banner-tabs button {
background: transparent;
border: none;
color: #ccc;
padding: 4px 10px;
cursor: pointer;
font-size: 12px;
}
.banner-tabs button:hover {
background: rgba(255,255,255,0.1);
}
.banner-tabs button.active {
background: rgba(255,255,255,0.2);
color: #fff;
}
#terminal-container {
height: 60%;
margin: 0 auto;
padding: 2px;
}
p {
@@ -44,45 +78,79 @@ pre {
#container {
display: flex;
position: relative;
min-height: 0;
}
.grid {
flex: 1;
/* max-height: 80vh;
overflow-y: auto; */
width: 100%;
min-width: 100px;
}
div:first-of-type.grid {
flex: 2;
height: 60vh;
height: 100%;
}
.tab {
overflow: hidden;
/* Sidebar */
#sidebar {
position: fixed;
top: 24px;
right: 0;
width: 400px;
height: 500px;
background: #fff;
border: 1px solid #ccc;
background-color: #f1f1f1;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 1000;
overflow: auto;
flex: none;
opacity: 0.5;
transition: opacity 0.1s;
}
#sidebar:hover {
opacity: 1;
}
#sidebar.sidebar-hidden {
display: none;
}
/* Style the buttons inside the tab */
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
font-size: 17px;
/* Resize handles */
#sidebar-resize-handle-horizontal {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
cursor: ew-resize;
background: transparent;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
#sidebar-resize-handle-horizontal:hover {
background: rgba(0, 120, 212, 0.3);
}
#sidebar-resize-handle-vertical {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 6px;
cursor: ns-resize;
background: transparent;
}
#sidebar-resize-handle-vertical:hover {
background: rgba(0, 120, 212, 0.3);
}
#sidebar-resize-handle-corner {
position: absolute;
left: 0;
bottom: 0;
width: 12px;
height: 12px;
cursor: nesw-resize;
background: transparent;
}
#sidebar-resize-handle-corner:hover {
background: rgba(0, 120, 212, 0.5);
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
/* Style the tab content */
.tabContent {
@@ -92,7 +160,7 @@ div:first-of-type.grid {
border-top: none;
}
#texture-atlas-zoom:checked + label + #texture-atlas canvas {
#texture-atlas-zoom:checked ~ #texture-atlas canvas {
/* Zoom atlas to the width of the container*/
width: 100% !important;
height: auto !important;