diff --git a/demo/client/client.ts b/demo/client/client.ts index 63da8fca..e484d8ca 100644 --- a/demo/client/client.ts +++ b/demo/client/client.ts @@ -19,6 +19,7 @@ import { AddonImageWindow } from './components/window/addonImageWindow'; import { AddonSearchWindow } from './components/window/addonSearchWindow'; import { AddonSerializeWindow } from './components/window/addonSerializeWindow'; import { AddonsWindow } from './components/window/addonsWindow'; +import { CellInspectorWindow } from './components/window/cellInspectorWindow'; import { ControlBar } from './components/controlBar'; import { WebglWindow } from './components/window/webglWindow'; import { OptionsWindow } from './components/window/optionsWindow'; @@ -206,6 +207,7 @@ if (document.location.pathname === '/test') { controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), []); optionsWindow = controlBar.registerWindow(new OptionsWindow(typedTerm, addons, { updateTerminalSize, updateTerminalContainerBackground })); const styleWindow = controlBar.registerWindow(new StyleWindow(typedTerm, addons)); + controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons)); controlBar.registerWindow(new VtWindow(typedTerm, addons)); addonsWindow = controlBar.registerWindow(new AddonsWindow(typedTerm, addons)); addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true }); diff --git a/demo/client/components/window/cellInspectorWindow.ts b/demo/client/components/window/cellInspectorWindow.ts new file mode 100644 index 00000000..8d3b0330 --- /dev/null +++ b/demo/client/components/window/cellInspectorWindow.ts @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { BaseWindow } from './baseWindow'; +import type { IControlWindow } from '../controlBar'; +import type { Terminal, IBufferCell } from '@xterm/xterm'; +import type { AddonCollection } from '../../types'; + +export class CellInspectorWindow extends BaseWindow implements IControlWindow { + public readonly id = 'cell-inspector'; + public readonly label = 'Cell Inspector'; + + private _container: HTMLElement; + private _positionEl: HTMLElement; + private _charEl: HTMLElement; + private _codeEl: HTMLElement; + private _widthEl: HTMLElement; + private _fgEl: HTMLElement; + private _bgEl: HTMLElement; + private _attrsEl: HTMLElement; + + constructor( + terminal: Terminal, + addons: AddonCollection, + ) { + super(terminal, addons); + } + + public build(container: HTMLElement): void { + this._container = container; + + const dl = document.createElement('dl'); + dl.style.fontFamily = 'monospace'; + dl.style.fontSize = '12px'; + + this._positionEl = this._addRow(dl, 'Position'); + this._charEl = this._addRow(dl, 'Character'); + this._codeEl = this._addRow(dl, 'Codepoint'); + this._widthEl = this._addRow(dl, 'Width'); + this._fgEl = this._addRow(dl, 'Foreground'); + this._bgEl = this._addRow(dl, 'Background'); + this._attrsEl = this._addRow(dl, 'Attributes'); + + container.appendChild(dl); + + // Add mouse move listener + this._setupMouseListener(); + } + + private _addRow(dl: HTMLElement, label: string): HTMLElement { + const dt = document.createElement('dt'); + dt.textContent = label; + dt.style.fontWeight = 'bold'; + dt.style.marginTop = '4px'; + dl.appendChild(dt); + + const dd = document.createElement('dd'); + dd.textContent = '-'; + dd.style.margin = '0 0 0 16px'; + dl.appendChild(dd); + + return dd; + } + + private _setupMouseListener(): void { + const terminal = this._terminal; + if (!terminal.element) { + return; + } + + const cell = terminal.buffer.active.getNullCell(); + + terminal.element.addEventListener('mousemove', (e: MouseEvent) => { + const core = (terminal as any)._core; + const coords = core._mouseService?.getCoords(e, core.screenElement, terminal.cols, terminal.rows); + if (!coords) { + this._clearDisplay(); + return; + } + + const x = coords[0] - 1; + const y = coords[1] - 1; + const bufferY = terminal.buffer.active.viewportY + y; + + const line = terminal.buffer.active.getLine(bufferY); + if (!line) { + this._clearDisplay(); + return; + } + + const cellData = line.getCell(x, cell); + if (!cellData) { + this._clearDisplay(); + return; + } + + this._updateDisplay(x, y, bufferY, cellData); + }); + + terminal.element.addEventListener('mouseleave', () => { + this._clearDisplay(); + }); + } + + private _clearDisplay(): void { + this._positionEl.textContent = '-'; + this._charEl.textContent = '-'; + this._codeEl.textContent = '-'; + this._widthEl.textContent = '-'; + this._fgEl.textContent = '-'; + this._bgEl.textContent = '-'; + this._attrsEl.textContent = '-'; + } + + private _updateDisplay(x: number, y: number, bufferY: number, cell: IBufferCell): void { + // Position + this._positionEl.textContent = `x=${x}, y=${y} (buffer: ${bufferY})`; + + // Character + const chars = cell.getChars(); + this._charEl.textContent = chars.length > 0 ? `"${chars}"` : '(empty)'; + + // Codepoint(s) + if (chars.length > 0) { + const codepoints = [...chars].map(c => { + const cp = c.codePointAt(0)!; + return `U+${cp.toString(16).toUpperCase().padStart(4, '0')}`; + }); + this._codeEl.textContent = codepoints.join(' '); + } else { + this._codeEl.textContent = '(none)'; + } + + // Width + this._widthEl.textContent = cell.getWidth().toString(); + + // Foreground color + this._fgEl.textContent = this._formatFgColor( + cell.getFgColor(), + cell.isFgDefault(), + cell.isFgPalette(), + cell.isFgRGB() + ); + + // Background color + this._bgEl.textContent = this._formatBgColor( + cell.getBgColor(), + cell.isBgDefault(), + cell.isBgPalette(), + cell.isBgRGB() + ); + + // Attributes (SGR codes) + const attrs: string[] = []; + if (cell.isBold()) attrs.push('bold (1)'); + if (cell.isDim()) attrs.push('dim (2)'); + if (cell.isItalic()) attrs.push('italic (3)'); + if (cell.isUnderline()) attrs.push('underline (4)'); + if (cell.isBlink()) attrs.push('blink (5)'); + if (cell.isInverse()) attrs.push('inverse (7)'); + if (cell.isInvisible()) attrs.push('invisible (8)'); + if (cell.isStrikethrough()) attrs.push('strikethrough (9)'); + if (cell.isOverline()) attrs.push('overline (53)'); + this._attrsEl.textContent = attrs.length > 0 ? attrs.join(', ') : '(none)'; + } + + private _formatFgColor(color: number, isDefault: boolean, isPalette: boolean, isRGB: boolean): string { + if (isDefault) { + return 'default (39)'; + } + if (isPalette) { + // SGR 30-37 for colors 0-7, 90-97 for colors 8-15, or 38;5;n for 0-255 + let sgr: string; + if (color < 8) { + sgr = `${30 + color}`; + } else if (color < 16) { + sgr = `${90 + color - 8}`; + } else { + sgr = `38;5;${color}`; + } + return `palette(${color}) (${sgr})`; + } + if (isRGB) { + const r = (color >> 16) & 0xFF; + const g = (color >> 8) & 0xFF; + const b = color & 0xFF; + return `#${color.toString(16).toUpperCase().padStart(6, '0')} (38;2;${r};${g};${b})`; + } + return `unknown(${color})`; + } + + private _formatBgColor(color: number, isDefault: boolean, isPalette: boolean, isRGB: boolean): string { + if (isDefault) { + return 'default (49)'; + } + if (isPalette) { + // SGR 40-47 for colors 0-7, 100-107 for colors 8-15, or 48;5;n for 0-255 + let sgr: string; + if (color < 8) { + sgr = `${40 + color}`; + } else if (color < 16) { + sgr = `${100 + color - 8}`; + } else { + sgr = `48;5;${color}`; + } + return `palette(${color}) (${sgr})`; + } + if (isRGB) { + const r = (color >> 16) & 0xFF; + const g = (color >> 8) & 0xFF; + const b = color & 0xFF; + return `#${color.toString(16).toUpperCase().padStart(6, '0')} (48;2;${r};${g};${b})`; + } + return `unknown(${color})`; + } +}