mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
@@ -116,7 +116,7 @@ describe('SerializeAddon', () => {
|
||||
describe('underline styles', () => {
|
||||
it('should serialize single underline with style', async () => {
|
||||
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:1mtest\u001b[0m');
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize double underline', async () => {
|
||||
|
||||
@@ -89,11 +89,19 @@ function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell)
|
||||
if (!cell1.isUnderline() && !cell2.isUnderline()) {
|
||||
return true;
|
||||
}
|
||||
const cell1Data = cell1 as unknown as IAttributeData;
|
||||
const cell2Data = cell2 as unknown as IAttributeData;
|
||||
return cell1Data.getUnderlineStyle() === cell2Data.getUnderlineStyle()
|
||||
&& cell1Data.getUnderlineColor() === cell2Data.getUnderlineColor()
|
||||
&& cell1Data.getUnderlineColorMode() === cell2Data.getUnderlineColorMode();
|
||||
if (cell1.getUnderlineStyle() !== cell2.getUnderlineStyle()) {
|
||||
return false;
|
||||
}
|
||||
const cell1Default = cell1.isUnderlineColorDefault();
|
||||
const cell2Default = cell2.isUnderlineColorDefault();
|
||||
if (cell1Default && cell2Default) {
|
||||
return true;
|
||||
}
|
||||
if (cell1Default !== cell2Default) {
|
||||
return false;
|
||||
}
|
||||
return cell1.getUnderlineColor() === cell2.getUnderlineColor()
|
||||
&& cell1.getUnderlineColorMode() === cell2.getUnderlineColorMode();
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
@@ -109,6 +117,16 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
|
||||
&& cell1.isStrikethrough() === cell2.isStrikethrough();
|
||||
}
|
||||
|
||||
function attributesEquals(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
const cell1AsBufferCell = cell1 as IBufferCell;
|
||||
if (typeof cell1AsBufferCell.attributesEquals === 'function') {
|
||||
return cell1AsBufferCell.attributesEquals(cell2);
|
||||
}
|
||||
return equalFg(cell1, cell2)
|
||||
&& equalBg(cell1, cell2)
|
||||
&& equalFlags(cell1, cell2);
|
||||
}
|
||||
|
||||
class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _rowIndex: number = 0;
|
||||
private _allRows: string[] = new Array<string>();
|
||||
@@ -258,6 +276,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
|
||||
const sgrSeq: number[] = [];
|
||||
if (attributesEquals(cell, oldCell)) {
|
||||
return sgrSeq;
|
||||
}
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
const flagsChanged = !equalFlags(cell, oldCell);
|
||||
@@ -290,17 +311,18 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||
if (!equalUnderline(cell, oldCell)) {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
const style = cellData.getUnderlineStyle();
|
||||
const style = cell.getUnderlineStyle();
|
||||
if (style === UnderlineStyle.NONE) {
|
||||
sgrSeq.push(24);
|
||||
} else if (style === UnderlineStyle.SINGLE && cell.isUnderlineColorDefault()) {
|
||||
sgrSeq.push(4);
|
||||
} else {
|
||||
// Use SGR 4:X format for underline styles
|
||||
sgrSeq.push('4:' + style as unknown as number);
|
||||
// Handle underline color
|
||||
if (!cellData.isUnderlineColorDefault()) {
|
||||
const color = cellData.getUnderlineColor();
|
||||
if (cellData.isUnderlineColorRGB()) {
|
||||
if (!cell.isUnderlineColorDefault()) {
|
||||
const color = cell.getUnderlineColor();
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
|
||||
} else {
|
||||
sgrSeq.push('58:5:' + color as unknown as number);
|
||||
@@ -675,12 +697,11 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
|
||||
private _getUnderlineColor(cell: IBufferCell): string | undefined {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
if (cellData.isUnderlineColorDefault()) {
|
||||
if (cell.isUnderlineColorDefault()) {
|
||||
return undefined;
|
||||
}
|
||||
const color = cellData.getUnderlineColor();
|
||||
if (cellData.isUnderlineColorRGB()) {
|
||||
const color = cell.getUnderlineColor();
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
const rgb = [
|
||||
(color >> 16) & 255,
|
||||
(color >> 8) & 255,
|
||||
@@ -693,8 +714,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
|
||||
private _getUnderlineStyle(cell: IBufferCell): string {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
switch (cellData.getUnderlineStyle()) {
|
||||
switch (cell.getUnderlineStyle()) {
|
||||
case UnderlineStyle.SINGLE:
|
||||
return 'underline';
|
||||
case UnderlineStyle.DOUBLE:
|
||||
@@ -713,6 +733,10 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
||||
const content: string[] = [];
|
||||
|
||||
if (attributesEquals(cell, oldCell)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
const flagsChanged = !equalFlags(cell, oldCell);
|
||||
|
||||
@@ -203,6 +203,46 @@ test.describe('SerializeAddon', () => {
|
||||
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
test('buffer cell attributesEquals compares underline style and color', async () => {
|
||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
|
||||
const sameAttributes = await ctx.page.evaluate(`(() => {
|
||||
const line = window.term.buffer.active.getLine(0);
|
||||
const cellA = line?.getCell(0);
|
||||
const cellB = line?.getCell(1);
|
||||
if (!cellA || !cellB) {
|
||||
return undefined;
|
||||
}
|
||||
return cellA.attributesEquals(cellB);
|
||||
})()`);
|
||||
strictEqual(sameAttributes, true);
|
||||
|
||||
await ctx.page.evaluate(`window.term.reset()`);
|
||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_GREEN)}B${sgr(NORMAL)}`);
|
||||
const differentColor = await ctx.page.evaluate(`(() => {
|
||||
const line = window.term.buffer.active.getLine(0);
|
||||
const cellA = line?.getCell(0);
|
||||
const cellB = line?.getCell(1);
|
||||
if (!cellA || !cellB) {
|
||||
return undefined;
|
||||
}
|
||||
return cellA.attributesEquals(cellB);
|
||||
})()`);
|
||||
strictEqual(differentColor, false);
|
||||
|
||||
await ctx.page.evaluate(`window.term.reset()`);
|
||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINED, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
|
||||
const differentStyle = await ctx.page.evaluate(`(() => {
|
||||
const line = window.term.buffer.active.getLine(0);
|
||||
const cellA = line?.getCell(0);
|
||||
const cellB = line?.getCell(1);
|
||||
if (!cellA || !cellB) {
|
||||
return undefined;
|
||||
}
|
||||
return cellA.attributesEquals(cellB);
|
||||
})()`);
|
||||
strictEqual(differentStyle, false);
|
||||
});
|
||||
|
||||
test('serialize all rows of content with color256', async function(): Promise<any> {
|
||||
const rows = 32;
|
||||
const cols = 10;
|
||||
@@ -602,6 +642,9 @@ const BOLD = '1';
|
||||
const DIM = '2';
|
||||
const ITALIC = '3';
|
||||
const UNDERLINED = '4';
|
||||
const UNDERLINE_DOUBLE = '4:2';
|
||||
const UNDERLINE_COLOR_RED = '58;5;196';
|
||||
const UNDERLINE_COLOR_GREEN = '58;5;46';
|
||||
const BLINK = '5';
|
||||
const INVERSE = '7';
|
||||
const INVISIBLE = '8';
|
||||
|
||||
@@ -124,7 +124,7 @@ export class DomRendererRowFactory {
|
||||
// Process any joined character ranges as needed. Because of how the
|
||||
// ranges are produced, we know that they are valid for the characters
|
||||
// and attributes of our input.
|
||||
let cell = this._workCell;
|
||||
let cell: ICellData = this._workCell;
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0] && isValidJoinRange) {
|
||||
const range = joinedRanges.shift()!;
|
||||
// If the ligature's selection state is not consistent, don't join it. This helps the
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from
|
||||
import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { IBufferLine, IDisposable } from 'common/Types';
|
||||
import { IBufferLine, ICellData, IDisposable } from 'common/Types';
|
||||
import { getRangeLength } from 'common/buffer/BufferRange';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
@@ -1021,7 +1021,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* word logic.
|
||||
* @param cell The cell to check.
|
||||
*/
|
||||
private _isCharWordSeparator(cell: CellData): boolean {
|
||||
private _isCharWordSeparator(cell: ICellData): boolean {
|
||||
// Zero width characters are never separators as they are always to the
|
||||
// right of wide characters
|
||||
if (cell.getWidth() === 0) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { Attributes, BgFlags, FgFlags, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { assert } from 'chai';
|
||||
|
||||
function createStyledCell(char: string, underlineStyle: UnderlineStyle, underlineColor: number): CellData {
|
||||
const cell = new CellData();
|
||||
const fg = Attributes.CM_P256 | 12 | FgFlags.BOLD | FgFlags.UNDERLINE;
|
||||
cell.setFromCharData([fg, char, 1, char.charCodeAt(0)]);
|
||||
cell.bg = Attributes.CM_P16 | 2 | BgFlags.ITALIC;
|
||||
cell.extended.underlineStyle = underlineStyle;
|
||||
cell.extended.underlineColor = Attributes.CM_P256 | underlineColor;
|
||||
cell.updateExtended();
|
||||
return cell;
|
||||
}
|
||||
|
||||
describe('CellData', () => {
|
||||
describe('attributesEquals', () => {
|
||||
it('returns true for same attributes with different chars', () => {
|
||||
const cellA = createStyledCell('A', UnderlineStyle.DOUBLE, 45);
|
||||
const cellB = createStyledCell('B', UnderlineStyle.DOUBLE, 45);
|
||||
|
||||
assert.equal(cellA.attributesEquals(cellB), true);
|
||||
});
|
||||
|
||||
it('detects underline style changes', () => {
|
||||
const cellA = createStyledCell('A', UnderlineStyle.DOUBLE, 45);
|
||||
const cellB = createStyledCell('B', UnderlineStyle.SINGLE, 45);
|
||||
|
||||
assert.equal(cellA.attributesEquals(cellB), false);
|
||||
});
|
||||
|
||||
it('detects underline color changes', () => {
|
||||
const cellA = createStyledCell('A', UnderlineStyle.SINGLE, 45);
|
||||
const cellB = createStyledCell('B', UnderlineStyle.SINGLE, 46);
|
||||
|
||||
assert.equal(cellA.attributesEquals(cellB), false);
|
||||
});
|
||||
|
||||
it('ignores underline variant offsets', () => {
|
||||
const cellA = createStyledCell('A', UnderlineStyle.SINGLE, 45);
|
||||
const cellB = createStyledCell('B', UnderlineStyle.SINGLE, 45);
|
||||
cellA.extended.underlineVariantOffset = 1;
|
||||
cellB.extended.underlineVariantOffset = 3;
|
||||
cellA.updateExtended();
|
||||
cellB.updateExtended();
|
||||
|
||||
assert.equal(cellA.attributesEquals(cellB), true);
|
||||
});
|
||||
|
||||
it('ignores url ids', () => {
|
||||
const cellA = createStyledCell('A', UnderlineStyle.SINGLE, 45);
|
||||
const cellB = createStyledCell('B', UnderlineStyle.SINGLE, 45);
|
||||
cellA.extended.urlId = 1;
|
||||
cellB.extended.urlId = 2;
|
||||
cellA.updateExtended();
|
||||
cellB.updateExtended();
|
||||
|
||||
assert.equal(cellA.attributesEquals(cellB), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CharData, ICellData, IExtendedAttrs } from 'common/Types';
|
||||
import { CharData, IAttributeData, ICellData, IExtendedAttrs } from 'common/Types';
|
||||
import { stringFromCodePoint } from 'common/input/TextDecoder';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from 'common/buffer/Constants';
|
||||
import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
|
||||
import type { IBufferCell as IBufferCellApi } from '@xterm/xterm';
|
||||
|
||||
/**
|
||||
* CellData - represents a single Cell in the terminal buffer.
|
||||
@@ -91,4 +92,61 @@ export class CellData extends AttributeData implements ICellData {
|
||||
public getAsCharData(): CharData {
|
||||
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
||||
}
|
||||
|
||||
public attributesEquals(other: IBufferCellApi): boolean {
|
||||
if (this.getFgColorMode() !== other.getFgColorMode() || this.getFgColor() !== other.getFgColor()) {
|
||||
return false;
|
||||
}
|
||||
if (this.getBgColorMode() !== other.getBgColorMode() || this.getBgColor() !== other.getBgColor()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isInverse() !== other.isInverse()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isBold() !== other.isBold()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isUnderline() !== other.isUnderline()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isUnderline()) {
|
||||
const otherData = other as unknown as IAttributeData;
|
||||
if (this.getUnderlineStyle() !== otherData.getUnderlineStyle()) {
|
||||
return false;
|
||||
}
|
||||
const thisDefault = this.isUnderlineColorDefault();
|
||||
const otherDefault = otherData.isUnderlineColorDefault();
|
||||
if (!(thisDefault && otherDefault)) {
|
||||
if (thisDefault !== otherDefault) {
|
||||
return false;
|
||||
}
|
||||
if (this.getUnderlineColor() !== otherData.getUnderlineColor()) {
|
||||
return false;
|
||||
}
|
||||
if (this.getUnderlineColorMode() !== otherData.getUnderlineColorMode()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isOverline() !== other.isOverline()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isBlink() !== other.isBlink()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isInvisible() !== other.isInvisible()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isItalic() !== other.isItalic()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isDim() !== other.isDim()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isStrikethrough() !== other.isStrikethrough()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ export class BufferLineApiView implements IBufferLineApi {
|
||||
}
|
||||
|
||||
if (cell) {
|
||||
this._line.loadCell(x, cell as ICellData);
|
||||
this._line.loadCell(x, cell as unknown as ICellData);
|
||||
return cell;
|
||||
}
|
||||
return this._line.loadCell(x, new CellData());
|
||||
return this._line.loadCell(x, new CellData()) as unknown as IBufferCellApi;
|
||||
}
|
||||
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
|
||||
return this._line.translateToString(trimRight, startColumn, endColumn);
|
||||
|
||||
Vendored
+20
@@ -1218,6 +1218,26 @@ declare module '@xterm/headless' {
|
||||
|
||||
/** Whether the cell has the default attribute (no color or style). */
|
||||
isAttributeDefault(): boolean;
|
||||
|
||||
/** Gets the underline style, see {@link UnderlineStyle}. */
|
||||
getUnderlineStyle(): number;
|
||||
/** Gets the underline color number, following the same rules as foreground colors. */
|
||||
getUnderlineColor(): number;
|
||||
/** Gets the underline color mode, see {@link Attributes.CM_DEFAULT} etc. */
|
||||
getUnderlineColorMode(): number;
|
||||
/** Whether the cell is using the RGB underline color mode. */
|
||||
isUnderlineColorRGB(): boolean;
|
||||
/** Whether the cell is using the palette underline color mode. */
|
||||
isUnderlineColorPalette(): boolean;
|
||||
/** Whether the cell is using the default underline color mode. */
|
||||
isUnderlineColorDefault(): boolean;
|
||||
|
||||
/**
|
||||
* Compares the cell's attributes (colors and styles) with another cell.
|
||||
* This does not compare the cell's content and excludes URL ids and
|
||||
* underline variant offsets.
|
||||
*/
|
||||
attributesEquals(other: IBufferCell): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+20
@@ -1853,6 +1853,26 @@ declare module '@xterm/xterm' {
|
||||
|
||||
/** Whether the cell has the default attribute (no color or style). */
|
||||
isAttributeDefault(): boolean;
|
||||
|
||||
/** Gets the underline style, see {@link UnderlineStyle}. */
|
||||
getUnderlineStyle(): number;
|
||||
/** Gets the underline color number, following the same rules as foreground colors. */
|
||||
getUnderlineColor(): number;
|
||||
/** Gets the underline color mode, see {@link Attributes.CM_DEFAULT} etc. */
|
||||
getUnderlineColorMode(): number;
|
||||
/** Whether the cell is using the RGB underline color mode. */
|
||||
isUnderlineColorRGB(): boolean;
|
||||
/** Whether the cell is using the palette underline color mode. */
|
||||
isUnderlineColorPalette(): boolean;
|
||||
/** Whether the cell is using the default underline color mode. */
|
||||
isUnderlineColorDefault(): boolean;
|
||||
|
||||
/**
|
||||
* Compares the cell's attributes (colors and styles) with another cell.
|
||||
* This does not compare the cell's content and excludes URL ids and
|
||||
* underline variant offsets.
|
||||
*/
|
||||
attributesEquals(other: IBufferCell): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user