Register the windows
This commit is contained in:
+160
-29
@@ -3,41 +3,172 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export function initControlBar(): void {
|
||||
// Sidebar resize handling
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const resizeHandleH = document.getElementById('sidebar-resize-handle-horizontal');
|
||||
const resizeHandleV = document.getElementById('sidebar-resize-handle-vertical');
|
||||
const resizeHandleCorner = document.getElementById('sidebar-resize-handle-corner');
|
||||
let resizeMode: 'none' | 'horizontal' | 'vertical' | 'corner' = 'none';
|
||||
export interface ITabConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function startResize(mode: 'horizontal' | 'vertical' | 'corner', e: MouseEvent): void {
|
||||
resizeMode = mode;
|
||||
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();
|
||||
}
|
||||
|
||||
resizeHandleH.addEventListener('mousedown', (e: MouseEvent) => startResize('horizontal', e));
|
||||
resizeHandleV.addEventListener('mousedown', (e: MouseEvent) => startResize('vertical', e));
|
||||
resizeHandleCorner.addEventListener('mousedown', (e: MouseEvent) => startResize('corner', e));
|
||||
private _initTabs(tabs: ITabConfig[]): void {
|
||||
// Clear existing buttons
|
||||
this._tabContainer.innerHTML = '';
|
||||
|
||||
document.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
if (resizeMode === 'none') return;
|
||||
if (resizeMode === 'horizontal' || resizeMode === 'corner') {
|
||||
const newWidth = window.innerWidth - e.clientX - 10;
|
||||
sidebar.style.width = `${Math.max(200, newWidth)}px`;
|
||||
}
|
||||
if (resizeMode === 'vertical' || resizeMode === 'corner') {
|
||||
const rect = sidebar.getBoundingClientRect();
|
||||
const newHeight = e.clientY - rect.top;
|
||||
sidebar.style.height = `${Math.max(100, newHeight)}px`;
|
||||
}
|
||||
});
|
||||
// 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;
|
||||
}
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
resizeMode = 'none';
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
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): 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));
|
||||
this._tabContainer.appendChild(button);
|
||||
|
||||
// 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 get activeTabId(): string | null {
|
||||
return this._activeTabId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user