More refactors

This commit is contained in:
Daniel Imms
2025-12-25 03:41:47 -08:00
parent 6bde923f07
commit c046f5b4b1
4 changed files with 326 additions and 52 deletions
+22 -11
View File
@@ -19,6 +19,7 @@ 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';
@@ -61,6 +62,8 @@ let protocol;
let socketURL;
let socket;
let pid;
let controlBar: ControlBar;
let addonsWindow: AddonsWindow;
let gpuWindow: GpuWindow;
let optionsWindow: OptionsWindow;
let styleWindow: StyleWindow;
@@ -120,11 +123,10 @@ 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;
};
let paddingElement: HTMLInputElement;
@@ -240,13 +242,18 @@ if (document.location.pathname === '/test') {
window.WebLinksAddon = WebLinksAddon;
window.WebglAddon = WebglAddon;
} else {
const controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), [
{ id: 'addons', label: 'Addons' }
]);
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);
gpuWindow = new GpuWindow();
controlBar.registerWindow(gpuWindow);
styleWindow = new StyleWindow();
controlBar.registerWindow(styleWindow);
paddingElement = styleWindow.paddingElement;
@@ -254,6 +261,7 @@ if (document.location.pathname === '/test') {
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);
@@ -344,6 +352,7 @@ function createTerminal(): void {
try {
typedTerm.loadAddon(addons.webgl.instance);
term.open(terminalContainer);
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));
@@ -472,6 +481,7 @@ function initAddons(term: Terminal): void {
const fragment = document.createDocumentFragment();
function postInitWebgl(): void {
controlBar.setTabVisible('gpu', true);
setTimeout(() => {
gpuWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => gpuWindow.setTextureAtlas(e));
@@ -479,6 +489,7 @@ function initAddons(term: Terminal): void {
}, 500);
}
function preDisposeWebgl(): void {
controlBar.setTabVisible('gpu', false);
if (addons.webgl.instance.textureAtlas) {
addons.webgl.instance.textureAtlas.remove();
}
@@ -594,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);
}
+58 -2
View File
@@ -146,14 +146,35 @@ export class ControlBar {
}
}
public registerWindow(window: IControlWindow): void {
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));
this._tabContainer.appendChild(button);
// 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');
@@ -168,7 +189,42 @@ export class ControlBar {
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;
}
}
-39
View File
@@ -21,45 +21,6 @@
<div id="terminal-container"></div>
</div>
<div id="sidebar" class="grid">
<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>
</div>
<script src="dist/client-bundle.js" defer ></script>