Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
714fda4575 | ||
|
|
d07813da37 |
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@xterm/xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "6.0.0",
|
||||
"version": "6.0.0-uniterm.1",
|
||||
"main": "lib/xterm.js",
|
||||
"module": "lib/xterm.mjs",
|
||||
"style": "css/xterm.css",
|
||||
|
||||
@@ -35,6 +35,7 @@ import { IRenderer } from 'browser/renderer/shared/Types';
|
||||
import { CharSizeService } from 'browser/services/CharSizeService';
|
||||
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
|
||||
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
|
||||
import { CellColorOverrideStore, ICellColorSpan } from 'browser/services/CellColorOverrideStore';
|
||||
import { LinkProviderService } from 'browser/services/LinkProviderService';
|
||||
import { MouseService } from 'browser/services/MouseService';
|
||||
import { RenderService } from 'browser/services/RenderService';
|
||||
@@ -953,6 +954,29 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
return this._decorationService.registerDecoration(decorationOptions);
|
||||
}
|
||||
|
||||
public readonly cellColorOverrides: CellColorOverrideStore = new CellColorOverrideStore();
|
||||
|
||||
/**
|
||||
* Recolor a contiguous cell range on one absolute buffer line. Pass null to
|
||||
* clear the line. The affected viewport row is re-rendered lazily. Overrides
|
||||
* are keyed by the active buffer's absolute line index; the caller must
|
||||
* clear/re-set them when indices shift (scrollback trim, resize reflow) or
|
||||
* when the alternate buffer changes.
|
||||
*/
|
||||
public setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void {
|
||||
if (this.cellColorOverrides.set(lineY, spans)) {
|
||||
const row = lineY - this._bufferService.buffer.ydisp;
|
||||
if (row >= 0 && row < this._bufferService.rows) {
|
||||
this._renderService?.refreshRows(row, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public clearCellColorOverrides(): void {
|
||||
this.cellColorOverrides.clear();
|
||||
this._renderService?.refreshRows(0, this._bufferService.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the terminal has an active selection.
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from '@xterm/xterm';
|
||||
import { CellColorOverrideStore } from 'browser/services/CellColorOverrideStore';
|
||||
import { ICellColorSpan, IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from '@xterm/xterm';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from 'browser/Types';
|
||||
@@ -30,6 +31,7 @@ export class TestTerminal extends CoreBrowserTerminal {
|
||||
}
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
public readonly cellColorOverrides = new CellColorOverrideStore();
|
||||
public onBlur!: Event<void>;
|
||||
public onFocus!: Event<void>;
|
||||
public onA11yChar!: Event<string>;
|
||||
@@ -108,6 +110,12 @@ export class MockTerminal implements ITerminal {
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
public clearCellColorOverrides(): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IBuffer } from 'common/buffer/Types';
|
||||
import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';
|
||||
import { channels, css } from 'common/Color';
|
||||
import type { Event } from 'vs/base/common/event';
|
||||
import { CellColorOverrideStore } from 'browser/services/CellColorOverrideStore';
|
||||
|
||||
/**
|
||||
* A portion of the public API that are implemented identially internally and simply passed through.
|
||||
@@ -20,6 +21,7 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
|
||||
buffer: IBuffer;
|
||||
linkifier: ILinkifier2 | undefined;
|
||||
options: Required<ITerminalOptions>;
|
||||
cellColorOverrides: CellColorOverrideStore;
|
||||
|
||||
onBlur: Event<void>;
|
||||
onFocus: Event<void>;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AddonManager } from 'common/public/AddonManager';
|
||||
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
|
||||
import { ParserApi } from 'common/public/ParserApi';
|
||||
import { UnicodeApi } from 'common/public/UnicodeApi';
|
||||
import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
|
||||
import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling, ICellColorSpan } from '@xterm/xterm';
|
||||
import type { Event } from 'vs/base/common/event';
|
||||
|
||||
/**
|
||||
@@ -177,6 +177,13 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
this._verifyPositiveIntegers(decorationOptions.x ?? 0, decorationOptions.width ?? 0, decorationOptions.height ?? 0);
|
||||
return this._core.registerDecoration(decorationOptions);
|
||||
}
|
||||
public setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void {
|
||||
this._verifyIntegers(lineY);
|
||||
this._core.setCellColorOverrides(lineY, spans);
|
||||
}
|
||||
public clearCellColorOverrides(): void {
|
||||
this._core.clearCellColorOverrides();
|
||||
}
|
||||
public hasSelection(): boolean {
|
||||
return this._core.hasSelection();
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
this._register(this._themeService.onChangeColors(e => this._injectCss(e)));
|
||||
this._injectCss(this._themeService.colors);
|
||||
|
||||
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document);
|
||||
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._terminal.cellColorOverrides);
|
||||
|
||||
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._screenElement.appendChild(this._rowContainer);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { CellColorOverrideStore } from 'browser/services/CellColorOverrideStore';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
@@ -27,6 +28,7 @@ describe('DomRendererRowFactory', () => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
rowFactory = new DomRendererRowFactory(
|
||||
dom.window.document,
|
||||
new CellColorOverrideStore(),
|
||||
new MockCharacterJoinerService(),
|
||||
new MockOptionsService({ drawBoldTextInBrightColors: true }),
|
||||
new MockCoreBrowserService(),
|
||||
@@ -77,6 +79,7 @@ describe('DomRendererRowFactory', () => {
|
||||
coreBrowserService.isFocused = false;
|
||||
const rowFactory = new DomRendererRowFactory(
|
||||
dom.window.document,
|
||||
new CellColorOverrideStore(),
|
||||
new MockCharacterJoinerService(),
|
||||
new MockOptionsService({ drawBoldTextInBrightColors: true }),
|
||||
coreBrowserService,
|
||||
@@ -101,6 +104,7 @@ describe('DomRendererRowFactory', () => {
|
||||
coreService.isCursorInitialized = false;
|
||||
const rowFactory = new DomRendererRowFactory(
|
||||
dom.window.document,
|
||||
new CellColorOverrideStore(),
|
||||
new MockCharacterJoinerService(),
|
||||
new MockOptionsService(),
|
||||
new MockCoreBrowserService(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CellData } from 'common/buffer/CellData';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { channels, color } from 'common/Color';
|
||||
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { CellColorOverrideStore } from 'browser/services/CellColorOverrideStore';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
@@ -44,6 +45,7 @@ export class DomRendererRowFactory {
|
||||
|
||||
constructor(
|
||||
private readonly _document: Document,
|
||||
private readonly _colorOverrides: CellColorOverrideStore,
|
||||
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
|
||||
@@ -95,6 +97,13 @@ export class DomRendererRowFactory {
|
||||
let skipJoinedCheckUntilX = 0;
|
||||
const classes: string[] = [];
|
||||
|
||||
// Cell color overrides for this line (absolute row index). Looked up once
|
||||
// per row, then consumed with a moving pointer since x only advances.
|
||||
const colorOverrideSpans = this._colorOverrides.get(row);
|
||||
let colorOverrideIdx = 0;
|
||||
let colorOverride: IColor | undefined;
|
||||
let oldColorOverride: IColor | undefined;
|
||||
|
||||
const hasHover = linkStart !== -1 && linkEnd !== -1;
|
||||
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
@@ -154,6 +163,21 @@ export class DomRendererRowFactory {
|
||||
const isCursorCell = isCursorRow && x === cursorX;
|
||||
const isLinkHover = hasHover && x >= linkStart && x <= linkEnd;
|
||||
|
||||
// Resolve the color override covering this cell (if any). Spans are
|
||||
// sorted by column and don't overlap, so a single moving pointer works.
|
||||
if (colorOverrideSpans) {
|
||||
while (
|
||||
colorOverrideIdx < colorOverrideSpans.length &&
|
||||
colorOverrideSpans[colorOverrideIdx].col + colorOverrideSpans[colorOverrideIdx].width <= x
|
||||
) {
|
||||
colorOverrideIdx++;
|
||||
}
|
||||
const span = colorOverrideSpans[colorOverrideIdx];
|
||||
colorOverride = (span && x >= span.col) ? span.color : undefined;
|
||||
} else {
|
||||
colorOverride = undefined;
|
||||
}
|
||||
|
||||
let isDecorated = false;
|
||||
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
|
||||
isDecorated = true;
|
||||
@@ -194,6 +218,7 @@ export class DomRendererRowFactory {
|
||||
&& cell.extended.ext === oldExt
|
||||
&& isLinkHover === oldLinkHover
|
||||
&& spacing === oldSpacing
|
||||
&& colorOverride === oldColorOverride
|
||||
&& !isCursorCell
|
||||
&& !isJoined
|
||||
&& !isDecorated
|
||||
@@ -228,6 +253,7 @@ export class DomRendererRowFactory {
|
||||
oldLinkHover = isLinkHover;
|
||||
oldSpacing = spacing;
|
||||
oldIsInSelection = isInSelection;
|
||||
oldColorOverride = colorOverride;
|
||||
|
||||
if (isJoined) {
|
||||
// The DOM renderer colors the background of the cursor but for ligatures all cells are
|
||||
@@ -417,7 +443,14 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
|
||||
// Foreground
|
||||
switch (fgColorMode) {
|
||||
if (colorOverride) {
|
||||
// Cell color override: recolor the text directly, winning over the
|
||||
// cell/decoration colors but still running through the minimum
|
||||
// contrast adjustment like every other foreground source.
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, colorOverride, cell, bgOverride, colorOverride)) {
|
||||
this._addStyle(charElement, `color:${colorOverride.css}`);
|
||||
}
|
||||
} else switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColor } from 'common/Types';
|
||||
import { css } from 'common/Color';
|
||||
|
||||
/**
|
||||
* A single contiguous cell range on one buffer line that should render its
|
||||
* text in a specific color.
|
||||
*/
|
||||
export interface ICellColorSpan {
|
||||
/** Zero-based column of the first cell. */
|
||||
col: number;
|
||||
/** Number of cells covered (wide chars count as 2). */
|
||||
width: number;
|
||||
/** CSS color string (#rrggbb / rgb(...) etc.), parsed on acceptance. */
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface StoredSpan {
|
||||
col: number;
|
||||
width: number;
|
||||
color: IColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sparse per-line cell color overrides consumed by the DOM renderer's row
|
||||
* factory. Keyed by absolute buffer line index of the active buffer; the
|
||||
* owner is responsible for invalidating keys when the buffer shifts
|
||||
* (scrollback trim, resize reflow, alternate buffer switch).
|
||||
*/
|
||||
export class CellColorOverrideStore {
|
||||
private _overrides = new Map<number, StoredSpan[]>();
|
||||
|
||||
/** Replace the overrides of one buffer line. Returns true when the stored
|
||||
* state changed and the line needs a re-render. Invalid color strings and
|
||||
* zero-width spans are dropped. */
|
||||
public set(lineY: number, spans: ICellColorSpan[] | null): boolean {
|
||||
if (!spans || spans.length === 0) {
|
||||
return this._overrides.delete(lineY);
|
||||
}
|
||||
const stored: StoredSpan[] = [];
|
||||
for (const span of spans) {
|
||||
if (span.width <= 0) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
stored.push({ col: span.col, width: span.width, color: css.toColor(span.color) });
|
||||
} catch {
|
||||
// Unsupported color format — skip the span rather than fail the batch.
|
||||
}
|
||||
}
|
||||
if (stored.length === 0) {
|
||||
return this._overrides.delete(lineY);
|
||||
}
|
||||
const existing = this._overrides.get(lineY);
|
||||
if (
|
||||
existing &&
|
||||
existing.length === stored.length &&
|
||||
existing.every((e, i) =>
|
||||
e.col === stored[i].col &&
|
||||
e.width === stored[i].width &&
|
||||
e.color.css === stored[i].color.css
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
this._overrides.set(lineY, stored);
|
||||
return true;
|
||||
}
|
||||
|
||||
public get(lineY: number): StoredSpan[] | undefined {
|
||||
return this._overrides.get(lineY);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._overrides.clear();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type { IRenderService } from 'browser/services/Services';
|
||||
import type { ICoreTerminal, IDisposable, IMarker } from 'common/Types';
|
||||
import * as playwright from '@playwright/test';
|
||||
import { PageFunction } from 'playwright-core/types/structs';
|
||||
import { IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from '@xterm/xterm';
|
||||
import { ICellColorSpan, IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from '@xterm/xterm';
|
||||
|
||||
export interface ITestContext {
|
||||
browser: Browser;
|
||||
@@ -279,6 +279,8 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi
|
||||
public async resize(cols: number, rows: number): Promise<void> { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); }
|
||||
public async registerMarker(y?: number | undefined): Promise<IMarker> { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); }
|
||||
public async registerDecoration(decorationOptions: IDecorationOptions): Promise<IDecoration | undefined> { return this._page.evaluate(([term, decorationOptions]) => term.registerDecoration(decorationOptions), [await this.getHandle(), decorationOptions] as const); }
|
||||
public setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void { throw new Error('not supported via proxy'); }
|
||||
public async clearCellColorOverrides(): Promise<void> { return this._page.evaluate((term) => term.clearCellColorOverrides(), await this.getHandle()); }
|
||||
public async clearTextureAtlas(): Promise<void> { return this.evaluate(([term]) => term.clearTextureAtlas()); }
|
||||
// #endregion
|
||||
|
||||
|
||||
Vendored
+29
@@ -612,6 +612,19 @@ declare module '@xterm/xterm' {
|
||||
overviewRulerOptions?: IDecorationOverviewRulerOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* A contiguous cell range on one buffer line whose text should be rendered
|
||||
* in a specific color, used with {@link Terminal.setCellColorOverrides}.
|
||||
*/
|
||||
export interface ICellColorSpan {
|
||||
/** Zero-based column of the first cell. */
|
||||
col: number;
|
||||
/** Number of cells covered (wide characters count as 2 cells). */
|
||||
width: number;
|
||||
/** CSS color string (#rrggbb, rgb(...), etc.). */
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The set of localizable strings.
|
||||
*/
|
||||
@@ -1156,6 +1169,22 @@ declare module '@xterm/xterm' {
|
||||
*/
|
||||
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
|
||||
|
||||
/**
|
||||
* Recolor a contiguous range of cells on one buffer line by drawing the
|
||||
* covered text in the given color. Pass null for spans to clear the line.
|
||||
* Line indexes are absolute within the active buffer; the caller must
|
||||
* clear/re-set overrides when indexes shift (scrollback trim, resize
|
||||
* reflow) or when the alternate buffer changes.
|
||||
* @param lineY The absolute buffer line index (ydisp-independent).
|
||||
* @param spans The cell ranges and colors to apply, or null to clear.
|
||||
*/
|
||||
setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void;
|
||||
|
||||
/**
|
||||
* Clears all cell color overrides set by setCellColorOverrides.
|
||||
*/
|
||||
clearCellColorOverrides(): void;
|
||||
|
||||
/**
|
||||
* Gets whether the terminal has an active selection.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user