diff --git a/demo/client.ts b/demo/client.ts index e26a0e7d..8605be81 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -19,7 +19,12 @@ if ('WebAssembly' in window) { import { Terminal, ITerminalOptions, type IDisposable, type ITheme } from '@xterm/xterm'; import { AttachAddon } from '@xterm/addon-attach'; -import { initControlBar } from './components/controlBar'; +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'; @@ -56,7 +61,11 @@ let protocol; let socketURL; let socket; let pid; -let autoResize: boolean = true; +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'; @@ -117,7 +126,7 @@ const actionElements = { findPrevious: document.querySelector('#find-previous') as HTMLInputElement, findResults: document.querySelector('#find-results') }; -const paddingElement = document.getElementById('padding') as HTMLInputElement; +let paddingElement: HTMLInputElement; const xtermjsTheme = { foreground: '#F8F8F8', @@ -231,8 +240,21 @@ 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' } + ]); + optionsWindow = new OptionsWindow(updateTerminalSize, updateTerminalContainerBackground); + controlBar.registerWindow(optionsWindow); + gpuWindow = new GpuWindow(); + controlBar.registerWindow(gpuWindow); + styleWindow = new StyleWindow(); + controlBar.registerWindow(styleWindow); + paddingElement = styleWindow.paddingElement; + testWindow = new TestWindow(); + controlBar.registerWindow(testWindow); + vtWindow = new VtWindow(); + controlBar.registerWindow(vtWindow); createTerminal(); - initControlBar(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('create-new-window').addEventListener('click', createNewWindowButtonHandler); document.getElementById('serialize').addEventListener('click', serializeButtonHandler); @@ -255,7 +277,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(); @@ -323,10 +344,10 @@ 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)); + 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(); @@ -340,9 +361,10 @@ function createTerminal(): void { term.focus(); updateTerminalContainerBackground(); + vtWindow?.initTerminal(term); const resizeObserver = new ResizeObserver(entries => { - if (autoResize) { + if (optionsWindow.autoResize) { addons.fit.instance.fit(); } }); @@ -377,7 +399,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 @@ -441,186 +463,6 @@ 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 += '
'; - booleanOptions.forEach(o => { - html += `
`; - }); - html += '
'; - numberOptions.forEach(o => { - html += `
`; - }); - html += '
'; - Object.keys(stringOptions).forEach(o => { - if (o === 'colsRows') { - html += `
`; - } else if (stringOptions[o]) { - const selectedOption = o === 'theme' ? 'xtermjs' : term.options[o]; - html += `
`; - } else { - html += `
`; - } - }); - html += '
'; - - 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; - if (o === 'theme') { - updateTerminalContainerBackground(); - } - }); - }); -} - function updateTerminalContainerBackground(): void { const bg = term.options.theme?.background ?? '#000000'; terminalContainer.style.backgroundColor = bg; @@ -631,9 +473,9 @@ function initAddons(term: Terminal): void { function postInitWebgl(): void { 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 { @@ -773,9 +615,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; @@ -808,21 +650,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'); @@ -1456,65 +1284,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([ '', diff --git a/demo/components/controlBar.ts b/demo/components/controlBar.ts index fb4d08e7..ab55891e 100644 --- a/demo/components/controlBar.ts +++ b/demo/components/controlBar.ts @@ -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 = 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; + } } diff --git a/demo/components/window/gpuWindow.ts b/demo/components/window/gpuWindow.ts new file mode 100644 index 00000000..4fa74ff8 --- /dev/null +++ b/demo/components/window/gpuWindow.ts @@ -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`; + } +} diff --git a/demo/components/window/optionsWindow.ts b/demo/components/window/optionsWindow.ts new file mode 100644 index 00000000..7a5a24fc --- /dev/null +++ b/demo/components/window/optionsWindow.ts @@ -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 Terminal constructor or by using the Terminal.options 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 += '
'; + booleanOptions.forEach(o => { + html += `
`; + }); + html += '
'; + numberOptions.forEach(o => { + html += `
`; + }); + html += '
'; + Object.keys(stringOptions).forEach(o => { + if (o === 'colsRows') { + html += `
`; + } else if (stringOptions[o]) { + const selectedOption = o === 'theme' ? 'xtermjs' : term.options[o]; + html += `
`; + } else { + html += `
`; + } + }); + html += '
'; + + 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; + } +} diff --git a/demo/components/window/styleWindow.ts b/demo/components/window/styleWindow.ts new file mode 100644 index 00000000..7f9811b9 --- /dev/null +++ b/demo/components/window/styleWindow.ts @@ -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; + } +} diff --git a/demo/components/window/testWindow.ts b/demo/components/window/testWindow.ts new file mode 100644 index 00000000..5626751c --- /dev/null +++ b/demo/components/window/testWindow.ts @@ -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); + } +} diff --git a/demo/components/window/vtWindow.ts b/demo/components/window/vtWindow.ts new file mode 100644 index 00000000..18614e55 --- /dev/null +++ b/demo/components/window/vtWindow.ts @@ -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}`; + } +} diff --git a/demo/index.html b/demo/index.html index b943a7ae..389b85e6 100644 --- a/demo/index.html +++ b/demo/index.html @@ -14,28 +14,13 @@
-
-

Style

-
- - -
-
-
-

Test

-
-
-
Lifecycle
-
-
-
- -
Performance
-
-
-
-
- -
Styles
-
-
-
-
-
-
-
-
-
- -
Decorations
-
-
-
- -
Ligatures Addon
-
- -
Weblinks Addon
-
- -
Image Test
-
-
-
- -
Events Test
-
-
- -
Progress Addon
-
-
-
-
-
-
- -
-
-
-
-
State:
-
-
-
-
-

VT

-
-
-
-

GPU

- - -
-
-