Merge pull request #5525 from Tyriar/demo_hover

Implement cell inspector in demo
This commit is contained in:
Daniel Imms
2025-12-28 13:57:35 -08:00
committed by GitHub
2 changed files with 220 additions and 0 deletions
+2
View File
@@ -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 });
@@ -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})`;
}
}