Merge upstream/master - resolve IIPHandler.ts const enum conflict

This commit is contained in:
Anthony Kim
2026-02-12 13:47:48 -08:00
51 changed files with 602 additions and 184 deletions
+3 -2
View File
@@ -69,9 +69,10 @@ export class FitAddon implements ITerminalAddon, IFitApi {
return undefined;
}
const scrollbarWidth = (this._terminal.options.scrollback === 0
const showScrollbar = this._terminal.options.scrollbar?.showScrollbar ?? true;
const scrollbarWidth = (this._terminal.options.scrollback === 0 || !showScrollbar
? 0
: (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
+14 -11
View File
@@ -5,16 +5,19 @@
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
import { ImageRenderer } from './ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import Base64Decoder, { type DecodeStatus } from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
// limit hold memory in base64 decoder (encoded bytes)
const KEEP_DATA = 4194304;
const INITIAL_DATA = 1048576;
// Local mirror of const enum (esbuild can't inline const enums from external packages)
const DECODER_OK: DecodeStatus.OK = 0;
// Local const enum mirror - esbuild can't inline const enums from external packages
const enum DecoderConst {
// Limit held memory in base64 decoder (encoded bytes).
KEEP_DATA = 4194304,
// Initial buffer allocation for the decoder.
INITIAL_DATA = 1048576,
// Local mirror of const enum (esbuild can't inline const enums from external packages)
OK = 0
}
// default IIP header values
const DEFAULT_HEADER: IHeaderFields = {
@@ -41,8 +44,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private readonly _coreTerminal: ITerminalExt
) {
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
const initialBytes = Math.min(INITIAL_DATA, maxEncodedBytes);
this._dec = new Base64Decoder(KEEP_DATA, maxEncodedBytes, initialBytes);
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
}
public reset(): void {}
@@ -58,7 +61,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
if (this._aborted) return;
if (this._hp.state === HeaderState.END) {
if (this._dec.put(data.subarray(start, end)) !== DECODER_OK) {
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
}
@@ -75,7 +78,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return;
}
this._dec.init();
if (this._dec.put(data.subarray(dataPos, end)) !== DECODER_OK) {
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
}
@@ -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 () => {
+40 -16
View File
@@ -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';
+5 -2
View File
@@ -7,6 +7,7 @@ import { ICellData } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { rgba } from 'common/Color';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions';
// Work variables to avoid garbage collection
let $fg = 0;
@@ -42,7 +43,7 @@ export class CellColorResolver {
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
*/
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number): void {
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number, deviceCellHeight: number): void {
this.result.bg = cell.bg;
this.result.fg = cell.fg;
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
@@ -63,7 +64,9 @@ export class CellColorResolver {
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
$variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
}
if ($variantOffset === 0 && blockPatternCodepoints.has(code)) {
$variantOffset = ((x * deviceCellWidth) % 2) * 2 + ((y * deviceCellHeight) % 2);
}
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
+3 -1
View File
@@ -8,6 +8,7 @@ import { ITerminalOptions, Terminal } from '@xterm/xterm';
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
import { ICharAtlasConfig, ITextureAtlas } from './Types';
import { generateConfig, configEquals } from './CharAtlasUtils';
import type { ILogService } from 'common/services/Services';
interface ITextureAtlasCacheEntry {
atlas: ITextureAtlas;
@@ -67,8 +68,9 @@ export function acquireTextureAtlas(
}
const core: ITerminal = (terminal as any)._core;
const logService = (core as any)._logService as ILogService;
const newEntry: ITextureAtlasCacheEntry = {
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
atlas: new TextureAtlas(document, newConfig, core.unicodeService, logService),
config: newConfig,
ownedBy: [terminal]
};
+6 -4
View File
@@ -14,7 +14,7 @@ import { IdleTaskQueue } from 'common/TaskQueue';
import { IColor } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
import { IUnicodeService } from 'common/services/Services';
import { ILogService, IUnicodeService } from 'common/services/Services';
import { Emitter } from 'common/Event';
/**
@@ -88,7 +88,8 @@ export class TextureAtlas implements ITextureAtlas {
constructor(
private readonly _document: Document,
private readonly _config: ICharAtlasConfig,
private readonly _unicodeService: IUnicodeService
private readonly _unicodeService: IUnicodeService,
private readonly _logService: ILogService
) {
this._createNewPage();
this._tmpCanvas = createCanvas(
@@ -119,7 +120,7 @@ export class TextureAtlas implements ITextureAtlas {
private _doWarmUp(): void {
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
const queue = new IdleTaskQueue();
const queue = new IdleTaskQueue(this._logService);
for (let i = 33; i < 126; i++) {
queue.enqueue(() => {
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
@@ -525,7 +526,8 @@ export class TextureAtlas implements ITextureAtlas {
// Draw custom characters if applicable
let customGlyph = false;
if (this._config.customGlyphs !== false) {
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css);
const variantOffset = this._workAttributeData.getUnderlineVariantOffset();
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset);
}
// Whether to clear pixels based on a threshold difference between the glyph color and the
+1 -1
View File
@@ -500,7 +500,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// Load colors/resolve overrides into work colors
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width);
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width, this.dimensions.device.cell.height);
// Override colors for cursor cell
if (isCursorVisible && row === cursorY) {
@@ -644,8 +644,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
] },
// Diagonal fill characters (1FB98-1FB99)
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,0 L1,1 M0,.25 L.75,1 M0,.5 L.5,1 M0,.75 L.25,1 M.25,0 L1,.75 M.5,0 L1,.5 M.75,0 L1,.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,.25 L.25,0 M0,.5 L.5,0 M0,.75 L.75,0 M0,1 L1,0 M.25,1 L1,.25 M.5,1 L1,.5 M.75,1 L1,.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,-0.25 L1.25,1.25 M-0.25,0 L1,1.25 M-0.25,0.25 L0.75,1.25 M-0.25,0.5 L0.5,1.25 M0,-0.25 L1.25,1 M0.25,-0.25 L1.25,0.75 M0.5,-0.25 L1.25,0.5 M-0.25,0.75 L0.25,1.25 M0.75,-0.25 L1.25,0.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,0.5 L0.5,-0.25 M-0.25,0.75 L0.75,-0.25 M-0.25,1 L1,-0.25 M-0.25,1.25 L1.25,-0.25 M0,1.25 L1.25,0 M0.25,1.25 L1.25,0.25 M0.5,1.25 L1.25,0.5 M-0.25,0.25 L0.25,-0.25 M0.75,1.25 L1.25,0.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
// Smooth mosaic terminal graphic characters (1FB9A-1FB9B)
'\u{1FB9A}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: 'M0,0 L.5,.5 L0,1 L1,1 L.5,.5 L1,0', type: CustomGlyphVectorType.FILL } }, // UPPER AND LOWER TRIANGULAR HALF BLOCK
@@ -820,6 +820,27 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
// #endregion
};
export const blockPatternCodepoints = new Set<number>([
// Shade characters (2591-2593)
0x2591,
0x2592,
0x2593,
// Rectangular shade characters (1FB8C-1FB94)
0x1FB8C,
0x1FB8D,
0x1FB8E,
0x1FB8F,
0x1FB90,
0x1FB91,
0x1FB92,
0x1FB94,
// Triangular shade characters (1FB9C-1FB9F)
0x1FB9C,
0x1FB9D,
0x1FB9E,
0x1FB9F
]);
/**
* Generates a drawing function for sextant characters. Sextants are a 2x3 grid where each cell
* can be on or off.
@@ -22,14 +22,15 @@ export function tryDrawCustomGlyph(
deviceCharHeight: number,
fontSize: number,
devicePixelRatio: number,
backgroundColor?: string
backgroundColor?: string,
variantOffset: number = 0
): boolean {
const unifiedCharDefinition = customGlyphDefinitions[c];
if (unifiedCharDefinition) {
// Normalize to array for uniform handling
const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
for (const part of parts) {
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor);
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor, variantOffset);
}
return true;
}
@@ -48,7 +49,8 @@ function drawDefinitionPart(
deviceCharHeight: number,
fontSize: number,
devicePixelRatio: number,
backgroundColor?: string
backgroundColor?: string,
variantOffset: number = 0
): void {
// Handle scaleType - adjust dimensions and offset when scaling to character area
let drawWidth = deviceCellWidth;
@@ -74,7 +76,7 @@ function drawDefinitionPart(
drawBlockVectorChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
break;
case CustomGlyphDefinitionType.BLOCK_PATTERN:
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, variantOffset);
break;
case CustomGlyphDefinitionType.PATH_FUNCTION:
drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
@@ -437,7 +439,8 @@ function drawPatternChar(
xOffset: number,
yOffset: number,
deviceCellWidth: number,
deviceCellHeight: number
deviceCellHeight: number,
variantOffset: number = 0
): void {
let patternSet = cachedPatterns.get(charDefinition);
if (!patternSet) {
@@ -486,6 +489,15 @@ function drawPatternChar(
pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
patternSet.set(fillStyle, pattern);
}
// Apply pattern offset to ensure seamless tiling across cells when cell dimensions are odd.
// variantOffset encodes: bit 1 = x pixel shift, bit 0 = y pixel shift.
const dx = (variantOffset >> 1) & 1;
const dy = variantOffset & 1;
if (dx !== 0 || dy !== 0) {
pattern.setTransform(new DOMMatrix().translateSelf(-dx, -dy));
} else {
pattern.setTransform(new DOMMatrix());
}
ctx.fillStyle = pattern;
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
}
@@ -500,6 +512,11 @@ function drawPathFunctionCharacter(
devicePixelRatio: number,
strokeWidth?: number
): void {
ctx.save();
ctx.beginPath();
ctx.rect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
ctx.clip();
ctx.beginPath();
let actualInstructions: string;
if (typeof charDefinition === 'function') {
@@ -526,7 +543,7 @@ function drawPathFunctionCharacter(
if (!args[0] || !args[1]) {
continue;
}
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio), state);
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio, 0, 0, false), state);
state.lastCommand = type;
}
if (strokeWidth !== undefined) {
@@ -537,6 +554,7 @@ function drawPathFunctionCharacter(
ctx.fill();
}
ctx.closePath();
ctx.restore();
}
/**
@@ -685,7 +703,7 @@ const svgToCanvasInstructionMap: { [index: string]: (ctx: CanvasRenderingContext
}
};
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0, clampToCell: boolean = true): number[] {
const result = args.map(e => parseFloat(e) || parseInt(e));
if (result.length < 2) {
@@ -695,10 +713,11 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let x = 0; x < result.length; x += 2) {
// Translate from 0-1 to 0-cellWidth
result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio);
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
// clamp to the cell bounds.
if (doClamp && result[x] !== 0) {
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
const rounded = Math.round(result[x] + 0.5) - 0.5;
result[x] = clampToCell ? clamp(rounded, cellWidth, 0) : rounded;
}
// Apply the cell's offset (ie. x*cellWidth)
result[x] += xOffset + (leftPadding * devicePixelRatio);
@@ -707,10 +726,11 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let y = 1; y < result.length; y += 2) {
// Translate from 0-1 to 0-cellHeight
result[y] *= cellHeight;
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
// clamp to the cell bounds.
if (doClamp && result[y] !== 0) {
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
const rounded = Math.round(result[y] + 0.5) - 0.5;
result[y] = clampToCell ? clamp(rounded, cellHeight, 0) : rounded;
}
// Apply the cell's offset (ie. x*cellHeight)
result[y] += yOffset;
+1 -1
View File
@@ -284,7 +284,7 @@ function createTerminal(): Terminal {
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
term = new Terminal({
scrollbar: { showArrows: true },
scrollbar: { showScrollbar: true },
allowProposedApi: true,
windowsPty: isWindows ? {
// In a real scenario, these values should be verified on the backend
+25 -9
View File
@@ -115,17 +115,20 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
'documentOverride',
'linkHandler',
'logger',
'overviewRuler',
'quirks',
'theme',
'vtExtensions',
'windowOptions',
'windowsPty',
];
const nestedBooleanOptions: { label: string, parent: string, prop: string }[] = [
{ label: 'vtExtensions.kittyKeyboard', parent: 'vtExtensions', prop: 'kittyKeyboard' },
{ label: 'vtExtensions.kittySgrBoldFaintControl', parent: 'vtExtensions', prop: 'kittySgrBoldFaintControl' },
{ label: 'vtExtensions.win32InputMode', parent: 'vtExtensions', prop: 'win32InputMode' }
const nestedBooleanOptions: { label: string, path: string[], prop: string }[] = [
{ label: 'scrollbar.showScrollbar', path: ['scrollbar'], prop: 'showScrollbar' },
{ label: 'scrollbar.showArrows', path: ['scrollbar'], prop: 'showArrows' },
{ label: 'scrollbar.overviewRuler.showTopBorder', path: ['scrollbar', 'overviewRuler'], prop: 'showTopBorder' },
{ label: 'scrollbar.overviewRuler.showBottomBorder', path: ['scrollbar', 'overviewRuler'], prop: 'showBottomBorder' },
{ label: 'vtExtensions.kittyKeyboard', path: ['vtExtensions'], prop: 'kittyKeyboard' },
{ label: 'vtExtensions.kittySgrBoldFaintControl', path: ['vtExtensions'], prop: 'kittySgrBoldFaintControl' },
{ label: 'vtExtensions.win32InputMode', path: ['vtExtensions'], prop: 'win32InputMode' }
];
const stringOptions: { [key: string]: string[] | null } = {
cursorStyle: ['block', 'underline', 'bar'],
@@ -161,8 +164,10 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${(this._terminal.options as Record<string, unknown>)[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
const checked = (this._terminal.options as Record<string, Record<string, unknown> | undefined>)[parent]?.[prop] ?? false;
nestedBooleanOptions.forEach(({ label, path, prop }) => {
const options = this._terminal.options as Record<string, unknown>;
const parent = path.reduce<Record<string, unknown> | undefined>((acc, key) => (acc as Record<string, unknown> | undefined)?.[key] as Record<string, unknown> | undefined, options);
const checked = (parent as Record<string, unknown> | undefined)?.[prop] ?? false;
html += `<div class="option"><label><input id="opt-${label.replace('.', '-')}" type="checkbox" ${checked ? 'checked' : ''}/> ${label}</label></div>`;
});
html += '</div><div class="option-group">';
@@ -196,11 +201,22 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
}
});
});
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
nestedBooleanOptions.forEach(({ label, path, prop }) => {
const input = document.getElementById(`opt-${label.replace('.', '-')}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', label, input.checked);
(this._terminal.options as Record<string, unknown>)[parent] = { ...(this._terminal.options as Record<string, Record<string, unknown> | undefined>)[parent], [prop]: input.checked };
const options = this._terminal.options as Record<string, unknown>;
if (path.length === 1) {
const parentKey = path[0];
options[parentKey] = { ...(options[parentKey] as Record<string, unknown> | undefined), [prop]: input.checked };
return;
}
if (path.length === 2) {
const [parentKey, childKey] = path;
const parent = (options[parentKey] as Record<string, unknown> | undefined) ?? {};
const child = (parent[childKey] as Record<string, unknown> | undefined) ?? {};
options[parentKey] = { ...parent, [childKey]: { ...child, [prop]: input.checked } };
}
});
});
numberOptions.forEach(o => {
+9 -2
View File
@@ -397,6 +397,13 @@ function customGlyphAlignmentHandler(term: Terminal): void {
}
}
term.write('\x1b[0mTriangular fill tests:\x1b[36m\n\r');
term.write('1FB9C 1FB9D 1FB9E 1FB9F all\n\r');
term.write('\u{02592}\u{02592}\u{02592}\u{1FB9C} \u{1FB9D}\u{02592}\u{02592}\u{02592} \u{00020}\u{00020}\u{00020}\u{1FB9E} \u{1FB9F}\u{00020}\u{00020}\u{00020} \u{00020}\u{1FB9E}\u{1FB9F}\u{00020}\n\r');
term.write('\u{02592}\u{02592}\u{1FB9C}\u{00020} \u{00020}\u{1FB9D}\u{02592}\u{02592} \u{00020}\u{00020}\u{1FB9E}\u{02592} \u{02592}\u{1FB9F}\u{00020}\u{00020} \u{1FB9E}\u{02592}\u{02592}\u{1FB9F}\n\r');
term.write('\u{02592}\u{1FB9C}\u{00020}\u{00020} \u{00020}\u{00020}\u{1FB9D}\u{02592} \u{00020}\u{1FB9E}\u{02592}\u{02592} \u{02592}\u{02592}\u{1FB9F}\u{00020} \u{1FB9D}\u{02592}\u{02592}\u{1FB9C}\n\r');
term.write('\u{1FB9C}\u{00020}\u{00020}\u{00020} \u{00020}\u{00020}\u{00020}\u{1FB9D} \u{1FB9E}\u{02592}\u{02592}\u{02592} \u{02592}\u{02592}\u{02592}\u{1FB9F} \u{00020}\u{1FB9D}\u{1FB9C}\u{00020}\n\r');
term.write('\x1b[0mPowerline alignment tests:\n\r');
const powerlineLeftChars = ['\u{E0B2}', '\u{E0B3}', '\u{E0B6}', '\u{E0B7}', '\u{E0BA}', '\u{E0BB}', '\u{E0BE}', '\u{E0BF}', '\u{E0C2}', '\u{E0C3}', '\u{E0C5}', '\u{E0C7}', '\u{E0CA}', '\u{E0D4}'];
const powerlineRightChars = ['\u{E0B0}', '\u{E0B1}', '\u{E0B4}', '\u{E0B5}', '\u{E0B8}', '\u{E0B9}', '\u{E0BC}', '\u{E0BD}', '\u{E0C0}', '\u{E0C1}', '\u{E0C4}', '\u{E0C6}', '\u{E0C8}', '\u{E0D2}', '\u{E0CC}', '\u{E0CD}', '\u{E0CE}', '\u{E0CF}', '\u{E0D0}', '\u{E0D1}'];
@@ -738,7 +745,7 @@ function loadTestLongLines(term: Terminal, addons: AddonCollection): void {
}
function addDecoration(term: Terminal, dim: number = 1): void {
term.options['overviewRuler'] = { width: 14 };
term.options.scrollbar = { ...(term.options.scrollbar ?? {}), width: 14, overviewRuler: term.options.scrollbar?.overviewRuler ?? {} };
const marker = term.registerMarker(1);
const decoration = term.registerDecoration({
marker,
@@ -755,7 +762,7 @@ function addDecoration(term: Terminal, dim: number = 1): void {
}
function addOverviewRuler(term: Terminal): void {
term.options['overviewRuler'] = { width: 14 };
term.options.scrollbar = { ...(term.options.scrollbar ?? {}), width: 14, overviewRuler: term.options.scrollbar?.overviewRuler ?? {} };
term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } });
term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } });
term.registerDecoration({ marker: term.registerMarker(5), overviewRulerOptions: { color: '#729fcf' } });
+6 -3
View File
@@ -616,11 +616,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
}
this._register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
if (this.options.overviewRuler.width) {
const showScrollbar = this.options.scrollbar?.showScrollbar ?? true;
const overviewRulerWidth = this.options.scrollbar?.width;
if (showScrollbar && overviewRulerWidth) {
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
this.optionsService.onSpecificOptionChange('overviewRuler', value => {
if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) {
this.optionsService.onSpecificOptionChange('scrollbar', value => {
const shouldShow = (value?.showScrollbar ?? true) && !!value?.width;
if (!this._overviewRulerRenderer && shouldShow && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
});
+9 -2
View File
@@ -62,7 +62,7 @@ export class Viewport extends Disposable {
this._register(this._optionsService.onMultipleOptionChange([
'scrollSensitivity',
'fastScrollSensitivity',
'overviewRuler'
'scrollbar'
], () => this._scrollableElement.updateOptions(this._getChangeOptions())));
// Don't handle mouse wheel if wheel events are supported by the current mouse prototcol
this._register(coreMouseService.onProtocolChange(type => {
@@ -131,10 +131,17 @@ export class Viewport extends Disposable {
}
private _getChangeOptions(): IScrollableElementChangeOptions {
const showScrollbar = this._optionsService.rawOptions.scrollbar?.showScrollbar ?? true;
const showArrows = this._optionsService.rawOptions.scrollbar?.showArrows ?? false;
const verticalScrollbarSize = showScrollbar
? (this._optionsService.rawOptions.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH)
: 0;
return {
mouseWheelScrollSensitivity: this._optionsService.rawOptions.scrollSensitivity,
fastScrollSensitivity: this._optionsService.rawOptions.fastScrollSensitivity,
verticalScrollbarSize: this._optionsService.rawOptions.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH
vertical: showScrollbar ? ScrollbarVisibility.AUTO : ScrollbarVisibility.HIDDEN,
verticalScrollbarSize,
verticalHasArrows: showArrows
};
}
@@ -38,7 +38,12 @@ export class OverviewRulerRenderer extends Disposable {
private readonly _ctx: CanvasRenderingContext2D;
private readonly _colorZoneStore: IColorZoneStore = new ColorZoneStore();
private get _width(): number {
return this._optionsService.options.overviewRuler?.width || 0;
const scrollbar = this._optionsService.rawOptions.scrollbar;
const showScrollbar = scrollbar?.showScrollbar ?? true;
if (!showScrollbar) {
return 0;
}
return scrollbar?.width ?? 0;
}
private _animationFrame: number | undefined;
@@ -46,8 +51,6 @@ export class OverviewRulerRenderer extends Disposable {
private _shouldUpdateAnchor: boolean | undefined = true;
private _lastKnownBufferLength: number = 0;
private _containerHeight: number | undefined;
constructor(
private readonly _viewportElement: HTMLElement,
private readonly _screenElement: HTMLElement,
@@ -86,16 +89,10 @@ export class OverviewRulerRenderer extends Disposable {
}
}));
// Container height changed
this._register(this._renderService.onRender((): void => {
if (!this._containerHeight || this._containerHeight !== this._screenElement.clientHeight) {
this._queueRefresh(true);
this._containerHeight = this._screenElement.clientHeight;
}
}));
this._register(this._renderService.onDimensionsChange(() => this._queueRefresh(true)));
this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
this._register(this._optionsService.onSpecificOptionChange('overviewRuler', () => this._queueRefresh(true)));
this._register(this._optionsService.onSpecificOptionChange('scrollbar', () => this._queueRefresh(true)));
this._register(this._themeService.onChangeColors(() => this._queueRefresh()));
this._queueRefresh(true);
}
@@ -139,10 +136,12 @@ export class OverviewRulerRenderer extends Disposable {
}
private _refreshCanvasDimensions(): void {
const cssCanvasHeight = this._renderService.dimensions.css.canvas.height;
const deviceCanvasHeight = this._renderService.dimensions.device.canvas.height;
this._canvas.style.width = `${this._width}px`;
this._canvas.width = Math.round(this._width * this._coreBrowserService.dpr);
this._canvas.style.height = `${this._screenElement.clientHeight}px`;
this._canvas.height = Math.round(this._screenElement.clientHeight * this._coreBrowserService.dpr);
this._canvas.style.height = `${cssCanvasHeight}px`;
this._canvas.height = deviceCanvasHeight;
this._refreshDrawConstants();
this._refreshColorZonePadding();
}
@@ -176,10 +175,10 @@ export class OverviewRulerRenderer extends Disposable {
private _renderRulerOutline(): void {
this._ctx.fillStyle = this._themeService.colors.overviewRulerBorder.css;
this._ctx.fillRect(0, 0, Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
if (this._optionsService.rawOptions.overviewRuler.showTopBorder) {
if (this._optionsService.rawOptions.scrollbar?.overviewRuler?.showTopBorder) {
this._ctx.fillRect(Constants.OVERVIEW_RULER_BORDER_WIDTH, 0, this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH, Constants.OVERVIEW_RULER_BORDER_WIDTH);
}
if (this._optionsService.rawOptions.overviewRuler.showBottomBorder) {
if (this._optionsService.rawOptions.scrollbar?.overviewRuler?.showBottomBorder) {
this._ctx.fillRect(Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height - Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
}
}
+1
View File
@@ -83,6 +83,7 @@ export class Terminal extends Disposable implements ITerminalApi {
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._core.onDimensionsChange; }
public get element(): HTMLElement | undefined { return this._core.element; }
public get screenElement(): HTMLElement | undefined { return this._core.screenElement; }
public get parser(): IParser {
return this._parser ??= new ParserApi(this._core);
}
@@ -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
+2 -1
View File
@@ -82,10 +82,11 @@ export abstract class AbstractScrollbar extends Widget {
/**
* Creates the dom node for an arrow & adds it to the container
*/
protected _createArrow(opts: IScrollbarArrowOptions): void {
protected _createArrow(opts: IScrollbarArrowOptions): ScrollbarArrow {
const arrow = this._register(new ScrollbarArrow(opts));
this.domNode.domNode.appendChild(arrow.bgDomNode);
this.domNode.domNode.appendChild(arrow.domNode);
return arrow;
}
/**
@@ -314,6 +314,12 @@ export class SmoothScrollableElement extends Widget {
if (typeof newOptions.vertical !== 'undefined') {
this._options.vertical = newOptions.vertical;
}
if (typeof newOptions.horizontalHasArrows !== 'undefined') {
this._options.horizontalHasArrows = newOptions.horizontalHasArrows;
}
if (typeof newOptions.verticalHasArrows !== 'undefined') {
this._options.verticalHasArrows = newOptions.verticalHasArrows;
}
if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize;
}
@@ -128,8 +128,10 @@ export interface IScrollableElementChangeOptions {
scrollPredominantAxis?: boolean;
horizontal?: ScrollbarVisibility;
horizontalScrollbarSize?: number;
horizontalHasArrows?: boolean;
vertical?: ScrollbarVisibility;
verticalScrollbarSize?: number;
verticalHasArrows?: boolean;
scrollByPage?: boolean;
}
+9 -1
View File
@@ -35,7 +35,7 @@ export class ScrollbarState {
* For the vertical scrollbar: the height of the scrollbar's arrows.
* For the horizontal scrollbar: the width of the scrollbar's arrows.
*/
private readonly _arrowSize: number;
private _arrowSize: number;
// --- variables
/**
@@ -127,6 +127,14 @@ export class ScrollbarState {
this._scrollbarSize = Math.round(scrollbarSize);
}
public setArrowSize(arrowSize: number): void {
const iArrowSize = Math.round(arrowSize);
if (this._arrowSize !== iArrowSize) {
this._arrowSize = iArrowSize;
this._refreshComputedValues();
}
}
public setOppositeScrollbarSize(oppositeScrollbarSize: number): void {
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
}
+56 -21
View File
@@ -7,17 +7,22 @@ import { AbstractScrollbar, ISimplifiedPointerEvent, IScrollbarHost } from './ab
import { IScrollableElementResolvedOptions } from './scrollableElementOptions';
import { ScrollbarState } from './scrollbarState';
import { INewScrollPosition, Scrollable, ScrollbarVisibility, IScrollEvent } from './scrollable';
import type { ScrollbarArrow } from './scrollbarArrow';
export class VerticalScrollbar extends AbstractScrollbar {
private _arrowUp: ScrollbarArrow | undefined;
private _arrowDown: ScrollbarArrow | undefined;
private _arrowScrollDelta: number = 0;
constructor(scrollable: Scrollable, options: IScrollableElementResolvedOptions, host: IScrollbarHost) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
const hasArrows = options.verticalHasArrows;
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
(options.verticalHasArrows ? options.verticalScrollbarSize : 0),
(hasArrows ? options.verticalScrollbarSize : 0),
(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize),
0,
scrollDimensions.height,
@@ -30,26 +35,7 @@ export class VerticalScrollbar extends AbstractScrollbar {
scrollByPage: options.scrollByPage
});
if (options.verticalHasArrows) {
const arrowSize = options.verticalScrollbarSize;
const arrowDelta = 0;
this._createArrow({
className: 'xterm-scra xterm-arrow-up',
top: arrowDelta,
left: arrowDelta,
bgWidth: options.verticalScrollbarSize,
bgHeight: arrowSize,
handleActivate: () => this._arrowScroll(-arrowSize)
});
this._createArrow({
className: 'xterm-scra xterm-arrow-down',
bottom: arrowDelta,
left: arrowDelta,
bgWidth: options.verticalScrollbarSize,
bgHeight: arrowSize,
handleActivate: () => this._arrowScroll(arrowSize)
});
}
this._setArrows(hasArrows, options.verticalScrollbarSize);
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
}
@@ -98,7 +84,56 @@ export class VerticalScrollbar extends AbstractScrollbar {
this._scrollable.setScrollPositionNow({ scrollTop: currentPosition.scrollTop + delta });
}
private _setArrows(showArrows: boolean, size: number): void {
this._arrowScrollDelta = size;
if (!this._arrowUp || !this._arrowDown) {
const arrowDelta = 0;
this._arrowUp = this._createArrow({
className: 'xterm-scra xterm-arrow-up',
top: arrowDelta,
left: arrowDelta,
bgWidth: size,
bgHeight: size,
handleActivate: () => this._arrowScroll(-this._arrowScrollDelta)
});
this._arrowDown = this._createArrow({
className: 'xterm-scra xterm-arrow-down',
bottom: arrowDelta,
left: arrowDelta,
bgWidth: size,
bgHeight: size,
handleActivate: () => this._arrowScroll(this._arrowScrollDelta)
});
}
this._updateArrowSize(this._arrowUp, size);
this._updateArrowSize(this._arrowDown, size);
if (!this._arrowUp || !this._arrowDown) {
return;
}
const display = showArrows ? '' : 'none';
this._arrowUp.bgDomNode.style.display = display;
this._arrowUp.domNode.style.display = display;
this._arrowDown.bgDomNode.style.display = display;
this._arrowDown.domNode.style.display = display;
}
private _updateArrowSize(arrow: ScrollbarArrow | undefined, size: number): void {
if (!arrow) {
return;
}
arrow.bgDomNode.style.width = `${size}px`;
arrow.bgDomNode.style.height = `${size}px`;
arrow.domNode.style.width = `${size}px`;
arrow.domNode.style.height = `${size}px`;
}
public updateOptions(options: IScrollableElementResolvedOptions): void {
const arrowSize = options.verticalHasArrows ? options.verticalScrollbarSize : 0;
this._scrollbarState.setArrowSize(arrowSize);
this._setArrows(options.verticalHasArrows, options.verticalScrollbarSize);
this.updateScrollbarSize(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize);
this._scrollbarState.setOppositeScrollbarSize(0);
this._visibilityController.setVisibility(options.vertical);
+5 -2
View File
@@ -9,7 +9,7 @@ import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { DebouncedIdleTask } from 'common/TaskQueue';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { IBufferService, ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event';
interface ISelectionState {
@@ -27,7 +27,7 @@ export class RenderService extends Disposable implements IRenderService {
private _renderer: MutableDisposable<IRenderer> = this._register(new MutableDisposable());
private _renderDebouncer: IRenderDebouncerWithCallback;
private _pausedResizeTask = new DebouncedIdleTask();
private _pausedResizeTask: DebouncedIdleTask;
private _observerDisposable = this._register(new MutableDisposable());
private _isPaused: boolean = false;
@@ -58,6 +58,7 @@ export class RenderService extends Disposable implements IRenderService {
private _rowCount: number,
screenElement: HTMLElement,
@IOptionsService private readonly _optionsService: IOptionsService,
@ILogService private readonly _logService: ILogService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@ICoreService private readonly _coreService: ICoreService,
@IDecorationService decorationService: IDecorationService,
@@ -67,6 +68,8 @@ export class RenderService extends Disposable implements IRenderService {
) {
super();
this._pausedResizeTask = new DebouncedIdleTask(this._logService);
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), this._coreBrowserService);
this._register(this._renderDebouncer);
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -107,10 +107,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._instantiationService = new InstantiationService();
this.optionsService = this._register(new OptionsService(options));
this._instantiationService.setService(IOptionsService, this.optionsService);
this._bufferService = this._register(this._instantiationService.createInstance(BufferService));
this._instantiationService.setService(IBufferService, this._bufferService);
this._logService = this._register(this._instantiationService.createInstance(LogService));
this._instantiationService.setService(ILogService, this._logService);
this._bufferService = this._register(this._instantiationService.createInstance(BufferService));
this._instantiationService.setService(IBufferService, this._bufferService);
this.coreService = this._register(this._instantiationService.createInstance(CoreService));
this._instantiationService.setService(ICoreService, this.coreService);
this.coreMouseService = this._register(this._instantiationService.createInstance(CoreMouseService));
+3 -3
View File
@@ -65,7 +65,7 @@ describe('InputHandler', () => {
beforeEach(() => {
optionsService = new MockOptionsService();
bufferService = new BufferService(optionsService);
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
oscLinkService = new OscLinkService(bufferService);
@@ -2458,7 +2458,7 @@ describe('InputHandler', () => {
beforeEach(() => {
optionsService = new MockOptionsService({ vtExtensions: { kittyKeyboard: true } });
bufferService = new BufferService(optionsService);
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
@@ -2508,7 +2508,7 @@ describe('InputHandler', () => {
beforeEach(() => {
optionsService = new MockOptionsService();
bufferService = new BufferService(optionsService);
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
coreService.onData(data => { console.log(data); });
+3 -2
View File
@@ -5,6 +5,7 @@
import { assert } from 'chai';
import { SortedList } from 'common/SortedList';
import { MockLogService } from 'common/TestUtils.test';
const deepStrictEqual = assert.deepStrictEqual;
@@ -15,7 +16,7 @@ describe('SortedList', () => {
}
beforeEach(() => {
list = new SortedList<number>(e => e);
list = new SortedList<number>(e => e, new MockLogService());
});
describe('insert', () => {
@@ -90,7 +91,7 @@ describe('SortedList', () => {
assertList([]);
});
it('custom key', () => {
const customList = new SortedList<{ key: number }>(e => e.key);
const customList = new SortedList<{ key: number }>(e => e.key, new MockLogService());
customList.insert({ key: 5 });
customList.insert({ key: 2 });
customList.insert({ key: 10 });
+7 -3
View File
@@ -4,6 +4,7 @@
*/
import { IdleTaskQueue } from 'common/TaskQueue';
import type { ILogService } from 'common/services/Services';
// Work variables to avoid garbage collection.
let i = 0;
@@ -18,16 +19,19 @@ export class SortedList<T> {
private _array: T[] = [];
private readonly _insertedValues: T[] = [];
private readonly _flushInsertedTask = new IdleTaskQueue();
private readonly _flushInsertedTask: InstanceType<typeof IdleTaskQueue>;
private _isFlushingInserted = false;
private readonly _deletedIndices: number[] = [];
private readonly _flushDeletedTask = new IdleTaskQueue();
private readonly _flushDeletedTask: InstanceType<typeof IdleTaskQueue>;
private _isFlushingDeleted = false;
constructor(
private readonly _getKey: (value: T) => number
private readonly _getKey: (value: T) => number,
logService: ILogService
) {
this._flushInsertedTask = new IdleTaskQueue(logService);
this._flushDeletedTask = new IdleTaskQueue(logService);
}
public clear(): void {
+9 -3
View File
@@ -4,6 +4,7 @@
*/
import { isNode } from 'common/Platform';
import type { ILogService } from 'common/services/Services';
interface ITaskQueue {
/**
@@ -34,6 +35,11 @@ abstract class TaskQueue implements ITaskQueue {
private _tasks: (() => boolean | void)[] = [];
private _idleCallback?: number;
private _i = 0;
protected readonly _logService: ILogService;
constructor(logService: ILogService) {
this._logService = logService;
}
protected abstract _requestCallback(callback: CallbackWithDeadline): number;
protected abstract _cancelCallback(identifier: number): void;
@@ -90,7 +96,7 @@ abstract class TaskQueue implements ITaskQueue {
// Warn when the time exceeding the deadline is over 20ms, if this happens in practice the
// task should be split into sub-tasks to ensure the UI remains responsive.
if (lastDeadlineRemaining - taskDuration < -20) {
console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);
this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);
}
this._start();
return;
@@ -151,8 +157,8 @@ export const IdleTaskQueue = (!isNode && 'requestIdleCallback' in window) ? Idle
export class DebouncedIdleTask {
private _queue: ITaskQueue;
constructor() {
this._queue = new IdleTaskQueue();
constructor(logService: ILogService) {
this._queue = new IdleTaskQueue(logService);
}
public set(task: () => boolean | void): void {
+1 -1
View File
@@ -27,7 +27,7 @@ export class MockBufferService implements IBufferService {
public rows: number,
optionsService: IOptionsService = new MockOptionsService()
) {
this.buffers = new BufferSet(optionsService, this);
this.buffers = new BufferSet(optionsService, this, new MockLogService());
// Listen to buffer activation events and automatically fire scroll events
this.buffers.onBufferActivate(e => {
this._onScroll.fire(e.activeBuffer.ydisp);
+7 -7
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { Buffer } from 'common/buffer/Buffer';
import { CircularList } from 'common/CircularList';
import { MockOptionsService, MockBufferService } from 'common/TestUtils.test';
import { MockOptionsService, MockBufferService, MockLogService } from 'common/TestUtils.test';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { ExtendedAttrs } from 'common/buffer/AttributeData';
@@ -23,7 +23,7 @@ describe('Buffer', () => {
beforeEach(() => {
optionsService = new MockOptionsService({ scrollback: INIT_SCROLLBACK });
bufferService = new MockBufferService(INIT_COLS, INIT_ROWS);
buffer = new Buffer(true, optionsService, bufferService);
buffer = new Buffer(true, optionsService, bufferService, new MockLogService());
});
describe('constructor', () => {
@@ -151,7 +151,7 @@ describe('Buffer', () => {
describe('no scrollback', () => {
it('should trim from the top of the buffer when the cursor reaches the bottom', () => {
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService);
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
assert.equal(buffer.lines.maxLength, INIT_ROWS);
buffer.y = INIT_ROWS - 1;
buffer.fillViewportRows();
@@ -1054,7 +1054,7 @@ describe('Buffer', () => {
describe('buffer marked to have no scrollback', () => {
it('should always have a scrollback of 0', () => {
// Test size on initialization
buffer = new Buffer(false, new MockOptionsService({ scrollback: 1000 }), bufferService);
buffer = new Buffer(false, new MockOptionsService({ scrollback: 1000 }), bufferService, new MockLogService());
buffer.fillViewportRows();
assert.equal(buffer.lines.maxLength, INIT_ROWS);
// Test size on buffer increase
@@ -1068,7 +1068,7 @@ describe('Buffer', () => {
describe('addMarker', () => {
it('should adjust a marker line when the buffer is trimmed', () => {
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService);
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
buffer.fillViewportRows();
const marker = buffer.addMarker(buffer.lines.length - 1);
assert.equal(marker.line, buffer.lines.length - 1);
@@ -1076,7 +1076,7 @@ describe('Buffer', () => {
assert.equal(marker.line, buffer.lines.length - 2);
});
it('should dispose of a marker if it is trimmed off the buffer', () => {
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService);
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
buffer.fillViewportRows();
assert.equal(buffer.markers.length, 0);
const marker = buffer.addMarker(0);
@@ -1088,7 +1088,7 @@ describe('Buffer', () => {
});
it('should call onDispose', () => {
const eventStack: string[] = [];
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService);
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
buffer.fillViewportRows();
assert.equal(buffer.markers.length, 0);
const marker = buffer.addMarker(0);
+6 -5
View File
@@ -14,7 +14,7 @@ import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR,
import { Marker } from 'common/buffer/Marker';
import { IBuffer } from 'common/buffer/Types';
import { DEFAULT_CHARSET } from 'common/data/Charsets';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBufferService, ILogService, IOptionsService } from 'common/services/Services';
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
@@ -48,11 +48,14 @@ export class Buffer implements IBuffer {
private _cols: number;
private _rows: number;
private _isClearing: boolean = false;
private _memoryCleanupQueue: InstanceType<typeof IdleTaskQueue>;
private _memoryCleanupPosition = 0;
constructor(
private _hasScrollback: boolean,
private _optionsService: IOptionsService,
private _bufferService: IBufferService
private _bufferService: IBufferService,
private readonly _logService: ILogService
) {
this._cols = this._bufferService.cols;
this._rows = this._bufferService.rows;
@@ -60,6 +63,7 @@ export class Buffer implements IBuffer {
this.scrollTop = 0;
this.scrollBottom = this._rows - 1;
this.setupTabStops();
this._memoryCleanupQueue = new IdleTaskQueue(this._logService);
}
public getNullCell(attr?: IAttributeData): ICellData {
@@ -277,9 +281,6 @@ export class Buffer implements IBuffer {
}
}
private _memoryCleanupQueue = new IdleTaskQueue();
private _memoryCleanupPosition = 0;
private _batchedMemoryCleanup(): boolean {
let normalRun = true;
if (this._memoryCleanupPosition >= this.lines.length) {
+3 -2
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { BufferSet } from 'common/buffer/BufferSet';
import { Buffer } from 'common/buffer/Buffer';
import { MockOptionsService, MockBufferService } from 'common/TestUtils.test';
import { MockOptionsService, MockBufferService, MockLogService } from 'common/TestUtils.test';
describe('BufferSet', () => {
let bufferSet: BufferSet;
@@ -14,7 +14,8 @@ describe('BufferSet', () => {
beforeEach(() => {
bufferSet = new BufferSet(
new MockOptionsService({ scrollback: 1000 }),
new MockBufferService(80, 24)
new MockBufferService(80, 24),
new MockLogService()
);
});
+5 -4
View File
@@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle';
import { IAttributeData } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBufferService, ILogService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event';
/**
@@ -27,7 +27,8 @@ export class BufferSet extends Disposable implements IBufferSet {
*/
constructor(
private readonly _optionsService: IOptionsService,
private readonly _bufferService: IBufferService
private readonly _bufferService: IBufferService,
private readonly _logService: ILogService
) {
super();
this.reset();
@@ -36,12 +37,12 @@ export class BufferSet extends Disposable implements IBufferSet {
}
public reset(): void {
this._normal = new Buffer(true, this._optionsService, this._bufferService);
this._normal = new Buffer(true, this._optionsService, this._bufferService, this._logService);
this._normal.fillViewportRows();
// The alt buffer should never have scrollback.
// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
this._alt = new Buffer(false, this._optionsService, this._bufferService);
this._alt = new Buffer(false, this._optionsService, this._bufferService, this._logService);
this._activeBuffer = this._normal;
this._onBufferActivate.fire({
activeBuffer: this._normal,
+65
View File
@@ -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);
});
});
});
+57
View File
@@ -7,6 +7,7 @@ import { CharData, 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,60 @@ 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()) {
if (this.getUnderlineStyle() !== other.getUnderlineStyle()) {
return false;
}
const thisDefault = this.isUnderlineColorDefault();
const otherDefault = other.isUnderlineColorDefault();
if (!(thisDefault && otherDefault)) {
if (thisDefault !== otherDefault) {
return false;
}
if (this.getUnderlineColor() !== other.getUnderlineColor()) {
return false;
}
if (this.getUnderlineColorMode() !== other.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;
}
}
+15 -1
View File
@@ -474,11 +474,25 @@ describe('KittyKeyboard', () => {
});
it('modifier key release includes its own bit cleared', () => {
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE);
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags | KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[57441;1:3u');
});
});
describe('modifier-only reporting', () => {
const flags = KittyKeyboardFlags.REPORT_EVENT_TYPES;
it('does not report modifier press without REPORT_ALL_KEYS_AS_ESCAPE_CODES', () => {
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags);
assert.strictEqual(result.key, undefined);
});
it('does not report modifier release without REPORT_ALL_KEYS_AS_ESCAPE_CODES', () => {
const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, undefined);
});
});
describe('REPORT_ALL_KEYS_AS_ESCAPE_CODES flag', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES;
+1 -1
View File
@@ -418,7 +418,7 @@ export class KittyKeyboard {
return result;
}
if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) && !reportEventTypes) {
if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES)) {
return result;
}
+2 -2
View File
@@ -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);
+6 -3
View File
@@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle';
import { IAttributeData, IBufferLine } from 'common/Types';
import { BufferSet } from 'common/buffer/BufferSet';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IBufferService, IOptionsService, type IBufferResizeEvent } from 'common/services/Services';
import { IBufferService, ILogService, IOptionsService, type IBufferResizeEvent } from 'common/services/Services';
import { Emitter } from 'common/Event';
export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
@@ -32,11 +32,14 @@ export class BufferService extends Disposable implements IBufferService {
/** An IBufferline to clone/copy from for new blank lines */
private _cachedBlankLine: IBufferLine | undefined;
constructor(@IOptionsService optionsService: IOptionsService) {
constructor(
@IOptionsService optionsService: IOptionsService,
@ILogService logService: ILogService
) {
super();
this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS);
this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
this.buffers = this._register(new BufferSet(optionsService, this));
this.buffers = this._register(new BufferSet(optionsService, this, logService));
this._register(this.buffers.onBufferActivate(e => {
this._onScroll.fire(e.activeBuffer.ydisp);
}));
@@ -8,6 +8,7 @@ import { DecorationService } from './DecorationService';
import { IMarker } from 'common/Types';
import { Disposable } from 'common/Lifecycle';
import { Emitter } from 'common/Event';
import { MockLogService } from 'common/TestUtils.test';
function createFakeMarker(line: number): IMarker {
return Object.freeze(new class extends Disposable {
@@ -22,7 +23,7 @@ const fakeMarker: IMarker = createFakeMarker(1);
describe('DecorationService', () => {
it('should set isDisposed to true after dispose', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: fakeMarker
});
@@ -34,7 +35,7 @@ describe('DecorationService', () => {
describe('forEachDecorationAtCell', () => {
it('should find decoration at its marker line', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10
@@ -47,7 +48,7 @@ describe('DecorationService', () => {
});
it('should find decoration with height > 1 on subsequent lines', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10,
@@ -73,7 +74,7 @@ describe('DecorationService', () => {
});
it('should not find decoration outside its x range', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
x: 5,
@@ -102,7 +103,7 @@ describe('DecorationService', () => {
describe('getDecorationsAtCell', () => {
it('should find decoration with height > 1 on subsequent lines', () => {
const service = new DecorationService();
const service = new DecorationService(new MockLogService());
const decoration = service.registerDecoration({
marker: createFakeMarker(5),
width: 10,
+5 -3
View File
@@ -5,7 +5,7 @@
import { css } from 'common/Color';
import { Disposable, DisposableStore, toDisposable } from 'common/Lifecycle';
import { IDecorationService, IInternalDecoration } from 'common/services/Services';
import { IDecorationService, IInternalDecoration, ILogService } from 'common/services/Services';
import { SortedList } from 'common/SortedList';
import { IColor } from 'common/Types';
import { IDecoration, IDecorationOptions, IMarker } from '@xterm/xterm';
@@ -25,7 +25,7 @@ export class DecorationService extends Disposable implements IDecorationService
* while marker line values do change, they should all change by the same amount so this should
* never become out of order.
*/
private readonly _decorations: SortedList<IInternalDecoration> = new SortedList(e => e?.marker.line);
private readonly _decorations: SortedList<IInternalDecoration>;
private readonly _onDecorationRegistered = this._register(new Emitter<IInternalDecoration>());
public readonly onDecorationRegistered = this._onDecorationRegistered.event;
@@ -34,9 +34,11 @@ export class DecorationService extends Disposable implements IDecorationService
public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
constructor() {
constructor(@ILogService private readonly _logService: ILogService) {
super();
this._decorations = new SortedList(e => e?.marker.line, this._logService);
this._register(toDisposable(() => this.reset()));
}
+1 -2
View File
@@ -32,7 +32,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
logLevel: 'info',
logger: null,
scrollback: 1000,
scrollbar: {},
scrollbar: { showScrollbar: true },
scrollOnEraseInDisplay: false,
scrollOnUserInput: true,
scrollSensitivity: 1,
@@ -55,7 +55,6 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
altClickMovesCursor: true,
convertEol: false,
termName: 'xterm',
overviewRuler: {},
quirks: {},
vtExtensions: {}
};
+2 -1
View File
@@ -9,6 +9,7 @@ import { BufferService } from 'common/services/BufferService';
import { OptionsService } from 'common/services/OptionsService';
import { OscLinkService } from 'common/services/OscLinkService';
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
import { MockLogService } from 'common/TestUtils.test';
describe('OscLinkService', () => {
describe('constructor', () => {
@@ -17,7 +18,7 @@ describe('OscLinkService', () => {
let oscLinkService: IOscLinkService;
beforeEach(() => {
optionsService = new OptionsService({ rows: 3, cols: 10 });
bufferService = new BufferService(optionsService);
bufferService = new BufferService(optionsService, new MockLogService());
oscLinkService = new OscLinkService(bufferService);
});
+3 -1
View File
@@ -265,7 +265,6 @@ export interface ITerminalOptions {
windowsPty?: IWindowsPty;
windowOptions?: IWindowOptions;
wordSeparator?: string;
overviewRuler?: IOverviewRulerOptions;
quirks?: ITerminalQuirks;
scrollbar?: IScrollbarOptions;
scrollOnEraseInDisplay?: boolean;
@@ -311,7 +310,10 @@ export interface ITerminalQuirks {
}
export interface IScrollbarOptions {
showScrollbar?: boolean;
showArrows?: boolean;
width?: number;
overviewRuler?: IOverviewRulerOptions;
}
export interface IVtExtensions {
+2 -2
View File
@@ -816,7 +816,7 @@ test.describe('API Integration Tests', () => {
});
test.describe('overviewRulerDecorations', () => {
test('should not add an overview ruler when width is not set', async () => {
await openTerminal(ctx);
await openTerminal(ctx, { scrollbar: { overviewRuler: {} } });
await ctx.page.evaluate(`window.marker1 = window.term.registerMarker(1)`);
await ctx.page.evaluate(`window.marker2 = window.term.registerMarker(2)`);
await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`);
@@ -824,7 +824,7 @@ test.describe('API Integration Tests', () => {
await pollFor(ctx.page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0);
});
test('should add an overview ruler when width is set', async () => {
await openTerminal(ctx, { overviewRuler: { width: 15 } });
await openTerminal(ctx, { scrollbar: { width: 15, overviewRuler: {} } });
await ctx.page.evaluate(`window.marker1 = window.term.registerMarker(1)`);
await ctx.page.evaluate(`window.marker2 = window.term.registerMarker(2)`);
await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`);
+1
View File
@@ -115,6 +115,7 @@ type TerminalProxyAsyncMethodOverrides = 'hasSelection' | 'getSelection' | 'getS
type TerminalProxyCustomOverrides = 'buffer' | 'dimensions' | (
// The below are not implemented yet
'element' |
'screenElement' |
'textarea' |
'markers' |
'unicode' |
+20
View File
@@ -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. */
getUnderlineStyle(): number;
/** Gets the underline color number. */
getUnderlineColor(): number;
/** Gets the underline color mode. */
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;
}
/**
+50 -18
View File
@@ -206,12 +206,6 @@ declare module '@xterm/xterm' {
*/
minimumContrastRatio?: number;
/**
* Controls the visibility and style of the overview ruler which visualizes
* decorations underneath the scroll bar.
*/
overviewRuler?: IOverviewRulerOptions;
/**
* Control various quirks features that are either non-standard or standard
* in but generally rejected in modern terminals.
@@ -399,8 +393,8 @@ declare module '@xterm/xterm' {
scrollbarSliderActiveBackground?: string;
/**
* The border color of the overview ruler. This visually separates the
* terminal from the scroll bar when {@link IOverviewRulerOptions.width} is
* set. When this is not set it defaults to black (`#000000`).
* terminal from the scroll bar when {@link IScrollbarOptions.width} is set.
* When this is not set it defaults to black (`#000000`).
*/
overviewRulerBorder?: string;
/** ANSI black (eg. `\x1b[30m`) */
@@ -688,8 +682,8 @@ declare module '@xterm/xterm' {
/**
* When defined, renders the decoration in the overview ruler to the right
* of the terminal. {@link IOverviewRulerOptions.width} must be set in order
* to see the overview ruler.
* of the terminal. {@link IScrollbarOptions.width} must be set in order to
* see the overview ruler.
* @param color The color of the decoration.
* @param position The position of the decoration.
*/
@@ -712,15 +706,10 @@ declare module '@xterm/xterm' {
tooMuchOutput: string;
}
/**
* Options for configuring the overview ruler rendered beside the scrollbar.
*/
export interface IOverviewRulerOptions {
/**
* When defined, renders decorations in the overview ruler to the right of
* the terminal. This must be set in order to see the overview ruler.
* @param color The color of the decoration.
* @param position The position of the decoration.
*/
width?: number;
/**
* Whether to show the top border of the overview ruler, which uses the
* {@link ITheme.overviewRulerBorder} color.
@@ -738,11 +727,28 @@ declare module '@xterm/xterm' {
* Options for configuring the scrollbar.
*/
export interface IScrollbarOptions {
/**
* Whether to show the scrollbar. When false, this supersedes
* {@link IScrollbarOptions.width}. Defaults to true.
*/
showScrollbar?: boolean;
/**
* Whether to show arrows at the top and bottom of the scrollbar. Defaults
* to false.
*/
showArrows?: boolean;
/**
* The width of the scrollbar and overview ruler in CSS pixels. When set,
* this enables the overview ruler.
*/
width?: number;
/**
* Controls the visibility and style of the overview ruler which visualizes
* decorations underneath the scroll bar.
*/
overviewRuler?: IOverviewRulerOptions;
}
/**
@@ -911,6 +917,12 @@ declare module '@xterm/xterm' {
*/
readonly element: HTMLElement | undefined;
/**
* The screen element containing the terminal's canvas rendering layers and
* decorations, excluding the viewport and the scrollbar.
*/
readonly screenElement: HTMLElement | undefined;
/**
* The textarea that accepts input for the terminal.
*/
@@ -1841,6 +1853,26 @@ declare module '@xterm/xterm' {
/** Whether the cell has the default attribute (no color or style). */
isAttributeDefault(): boolean;
/** Gets the underline style. */
getUnderlineStyle(): number;
/** Gets the underline color number. */
getUnderlineColor(): number;
/** Gets the underline color mode. */
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;
}
/**