Register the windows

This commit is contained in:
Daniel Imms
2025-12-24 17:09:20 -08:00
parent 40b2bbef34
commit 6bde923f07
8 changed files with 813 additions and 464 deletions
+160 -29
View File
@@ -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;
}
}
+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}`;
}
}