Merge upstream/master - resolve IIPHandler.ts const enum conflict
This commit is contained in:
@@ -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'));
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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' } });
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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); });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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' } })`);
|
||||
|
||||
@@ -115,6 +115,7 @@ type TerminalProxyAsyncMethodOverrides = 'hasSelection' | 'getSelection' | 'getS
|
||||
type TerminalProxyCustomOverrides = 'buffer' | 'dimensions' | (
|
||||
// The below are not implemented yet
|
||||
'element' |
|
||||
'screenElement' |
|
||||
'textarea' |
|
||||
'markers' |
|
||||
'unicode' |
|
||||
|
||||
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. */
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+50
-18
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user