mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #5453 from chrislloyd/feat/synchronized-output
Add synchronized output support (DEC mode 2026)
This commit is contained in:
@@ -490,6 +490,7 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
if (modes.originMode) content += '\x1b[?6h';
|
||||
if (modes.reverseWraparoundMode) content += '\x1b[?45h';
|
||||
if (modes.sendFocusMode) content += '\x1b[?1004h';
|
||||
// synchronizedOutputMode doesn't need to be serialized as it's a temporary mode
|
||||
|
||||
// Default: true
|
||||
if (modes.wraparoundMode === false) content += '\x1b[?7l';
|
||||
|
||||
+19
-17
@@ -1310,23 +1310,25 @@ function addVtButtons(): void {
|
||||
}
|
||||
const vtFragment = document.createDocumentFragment();
|
||||
const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = {
|
||||
A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
|
||||
B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
|
||||
C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
|
||||
D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
|
||||
E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
|
||||
F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
|
||||
G: { label: 'CHA', description: 'Cursor Character Absolute' },
|
||||
H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
|
||||
I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
|
||||
J: { label: 'ED', description: 'Erase in Display' },
|
||||
'?|J': { label: 'DECSED', description: 'Erase in Display' },
|
||||
K: { label: 'EL', description: 'Erase in Line' },
|
||||
'?|K': { label: 'DECSEL', description: 'Erase in Line' },
|
||||
L: { label: 'IL', description: 'Insert Ps Line(s)' },
|
||||
M: { label: 'DL', description: 'Delete Ps Line(s)' },
|
||||
P: { label: 'DCH', description: 'Delete Ps Character(s)' },
|
||||
' q': { label: 'DECSCUSR', description: 'Set Cursor Style', paramCount: 1 }
|
||||
A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
|
||||
B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
|
||||
C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
|
||||
D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
|
||||
E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
|
||||
F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
|
||||
G: { label: 'CHA', description: 'Cursor Character Absolute' },
|
||||
H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
|
||||
I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
|
||||
J: { label: 'ED', description: 'Erase in Display' },
|
||||
'?|J': { label: 'DECSED', description: 'Erase in Display' },
|
||||
K: { label: 'EL', description: 'Erase in Line' },
|
||||
'?|K': { label: 'DECSEL', description: 'Erase in Line' },
|
||||
L: { label: 'IL', description: 'Insert Ps Line(s)' },
|
||||
M: { label: 'DL', description: 'Delete Ps Line(s)' },
|
||||
P: { label: 'DCH', description: 'Delete Ps Character(s)' },
|
||||
' q': { label: 'DECSCUSR', description: 'Set Cursor Style' },
|
||||
'?2026h': { label: 'BSU', description: 'Begin synchronized update', paramCount: 0 },
|
||||
'?2026l': { label: 'ESU', description: 'End synchronized update', paramCount: 0 }
|
||||
};
|
||||
for (const s of Object.keys(buttonSpecs)) {
|
||||
const spec = buttonSpecs[s];
|
||||
|
||||
@@ -123,6 +123,7 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
originMode: m.origin,
|
||||
reverseWraparoundMode: m.reverseWraparound,
|
||||
sendFocusMode: m.sendFocus,
|
||||
synchronizedOutputMode: m.synchronizedOutput,
|
||||
wraparoundMode: m.wraparound
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 'vs/base/common/lifecycle';
|
||||
import { DebouncedIdleTask } from 'common/TaskQueue';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
interface ISelectionState {
|
||||
@@ -18,6 +18,10 @@ interface ISelectionState {
|
||||
columnSelectMode: boolean;
|
||||
}
|
||||
|
||||
const enum Constants {
|
||||
SYNCHRONIZED_OUTPUT_TIMEOUT_MS = 1000
|
||||
}
|
||||
|
||||
export class RenderService extends Disposable implements IRenderService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
@@ -32,6 +36,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
private _needsSelectionRefresh: boolean = false;
|
||||
private _canvasWidth: number = 0;
|
||||
private _canvasHeight: number = 0;
|
||||
private _syncOutputHandler: SynchronizedOutputHandler;
|
||||
private _selectionState: ISelectionState = {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
@@ -52,23 +57,31 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
constructor(
|
||||
private _rowCount: number,
|
||||
screenElement: HTMLElement,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IDecorationService decorationService: IDecorationService,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), coreBrowserService);
|
||||
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), this._coreBrowserService);
|
||||
this._register(this._renderDebouncer);
|
||||
|
||||
this._register(coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
|
||||
this._syncOutputHandler = new SynchronizedOutputHandler(
|
||||
this._coreBrowserService,
|
||||
this._coreService,
|
||||
() => this._fullRefresh()
|
||||
);
|
||||
this._register(toDisposable(() => this._syncOutputHandler.dispose()));
|
||||
|
||||
this._register(this._coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
|
||||
|
||||
this._register(bufferService.onResize(() => this._fullRefresh()));
|
||||
this._register(bufferService.buffers.onBufferActivate(() => this._renderer.value?.clear()));
|
||||
this._register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this._register(this._optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this._register(this._charSizeService.onCharSizeChange(() => this.handleCharSizeChanged()));
|
||||
|
||||
// Do a full refresh whenever any decoration is added or removed. This may not actually result
|
||||
@@ -78,7 +91,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._register(decorationService.onDecorationRemoved(() => this._fullRefresh()));
|
||||
|
||||
// Clear the renderer when the a change that could affect glyphs occurs
|
||||
this._register(optionsService.onMultipleOptionChange([
|
||||
this._register(this._optionsService.onMultipleOptionChange([
|
||||
'customGlyphs',
|
||||
'drawBoldTextInBrightColors',
|
||||
'letterSpacing',
|
||||
@@ -96,15 +109,15 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
}));
|
||||
|
||||
// Refresh the cursor line when the cursor changes
|
||||
this._register(optionsService.onMultipleOptionChange([
|
||||
this._register(this._optionsService.onMultipleOptionChange([
|
||||
'cursorBlink',
|
||||
'cursorStyle'
|
||||
], () => this.refreshRows(bufferService.buffer.y, bufferService.buffer.y, true)));
|
||||
|
||||
this._register(themeService.onChangeColors(() => this._fullRefresh()));
|
||||
|
||||
this._registerIntersectionObserver(coreBrowserService.window, screenElement);
|
||||
this._register(coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement)));
|
||||
this._registerIntersectionObserver(this._coreBrowserService.window, screenElement);
|
||||
this._register(this._coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement)));
|
||||
}
|
||||
|
||||
private _registerIntersectionObserver(w: Window & typeof globalThis, screenElement: HTMLElement): void {
|
||||
@@ -137,6 +150,18 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._needsFullRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._coreService.decPrivateModes.synchronizedOutput) {
|
||||
this._syncOutputHandler.bufferRows(start, end);
|
||||
return;
|
||||
}
|
||||
|
||||
const buffered = this._syncOutputHandler.flush();
|
||||
if (buffered) {
|
||||
start = Math.min(start, buffered.start);
|
||||
end = Math.max(end, buffered.end);
|
||||
}
|
||||
|
||||
if (!isRedrawOnly) {
|
||||
this._isNextRenderRedrawOnly = false;
|
||||
}
|
||||
@@ -148,6 +173,13 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip rendering if synchronized output mode is enabled. This check must happen here
|
||||
// (in addition to refreshRows) to handle renders that were queued before the mode was enabled.
|
||||
if (this._coreService.decPrivateModes.synchronizedOutput) {
|
||||
this._syncOutputHandler.bufferRows(start, end);
|
||||
return;
|
||||
}
|
||||
|
||||
// Since this is debounced, a resize event could have happened between the time a refresh was
|
||||
// requested and when this triggers. Clamp the values of start and end to ensure they're valid
|
||||
// given the current viewport state.
|
||||
@@ -283,3 +315,62 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._renderer.value?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffers row refresh requests during synchronized output mode (DEC mode 2026).
|
||||
* When the mode is disabled, the accumulated row range is flushed for rendering.
|
||||
* A safety timeout ensures rendering occurs even if the end sequence is not received.
|
||||
*/
|
||||
class SynchronizedOutputHandler {
|
||||
private _start: number = 0;
|
||||
private _end: number = 0;
|
||||
private _timeout: number | undefined;
|
||||
private _isBuffering: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _coreBrowserService: ICoreBrowserService,
|
||||
private readonly _coreService: ICoreService,
|
||||
private readonly _onTimeout: () => void
|
||||
) {}
|
||||
|
||||
public bufferRows(start: number, end: number): void {
|
||||
if (!this._isBuffering) {
|
||||
this._start = start;
|
||||
this._end = end;
|
||||
this._isBuffering = true;
|
||||
} else {
|
||||
this._start = Math.min(this._start, start);
|
||||
this._end = Math.max(this._end, end);
|
||||
}
|
||||
|
||||
if (this._timeout === undefined) {
|
||||
this._timeout = this._coreBrowserService.window.setTimeout(() => {
|
||||
this._timeout = undefined;
|
||||
this._coreService.decPrivateModes.synchronizedOutput = false;
|
||||
this._onTimeout();
|
||||
}, Constants.SYNCHRONIZED_OUTPUT_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
public flush(): { start: number, end: number } | undefined {
|
||||
if (this._timeout !== undefined) {
|
||||
this._coreBrowserService.window.clearTimeout(this._timeout);
|
||||
this._timeout = undefined;
|
||||
}
|
||||
|
||||
if (!this._isBuffering) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = { start: this._start, end: this._end };
|
||||
this._isBuffering = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._timeout !== undefined) {
|
||||
this._coreBrowserService.window.clearTimeout(this._timeout);
|
||||
this._timeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2314,7 +2314,7 @@ describe('InputHandler', () => {
|
||||
});
|
||||
it('DEC privates with set/reset semantic', async () => {
|
||||
// initially reset
|
||||
const reset = [1, 6, 9, 12, 45, 66, 1000, 1002, 1003, 1004, 1006, 1016, 47, 1047, 1049, 2004];
|
||||
const reset = [1, 6, 9, 12, 45, 66, 1000, 1002, 1003, 1004, 1006, 1016, 47, 1047, 1049, 2004, 2026];
|
||||
for (const mode of reset) {
|
||||
await inputHandler.parseP(`\x1b[?${mode}$p`);
|
||||
assert.deepEqual(reportStack.pop(), `\x1b[?${mode};2$y`); // initial reset
|
||||
|
||||
@@ -1969,6 +1969,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
|
||||
this._coreService.decPrivateModes.bracketedPasteMode = true;
|
||||
break;
|
||||
case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)
|
||||
this._coreService.decPrivateModes.synchronizedOutput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -2197,6 +2200,10 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
|
||||
this._coreService.decPrivateModes.bracketedPasteMode = false;
|
||||
break;
|
||||
case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)
|
||||
this._coreService.decPrivateModes.synchronizedOutput = false;
|
||||
this._onRequestRefreshRows.fire(undefined);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -2291,6 +2298,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (p === 1048) return f(p, V.SET); // xterm always returns SET here
|
||||
if (p === 47 || p === 1047 || p === 1049) return f(p, b2v(active === alt));
|
||||
if (p === 2004) return f(p, b2v(dm.bracketedPasteMode));
|
||||
if (p === 2026) return f(p, b2v(dm.synchronizedOutput));
|
||||
return f(p, V.NOT_RECOGNIZED);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ export class MockCoreService implements ICoreService {
|
||||
origin: false,
|
||||
reverseWraparound: false,
|
||||
sendFocus: false,
|
||||
synchronizedOutput: false,
|
||||
wraparound: true
|
||||
};
|
||||
public onData: Event<string> = new Emitter<string>().event;
|
||||
|
||||
@@ -273,6 +273,7 @@ export interface IDecPrivateModes {
|
||||
origin: boolean;
|
||||
reverseWraparound: boolean;
|
||||
sendFocus: boolean;
|
||||
synchronizedOutput: boolean;
|
||||
wraparound: boolean; // defaults: xterm - true, vt100 - false
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
|
||||
origin: false,
|
||||
reverseWraparound: false,
|
||||
sendFocus: false,
|
||||
synchronizedOutput: false,
|
||||
wraparound: true // defaults: xterm - true, vt100 - false
|
||||
});
|
||||
|
||||
|
||||
@@ -400,6 +400,7 @@ describe('Headless API Tests', function (): void {
|
||||
originMode: false,
|
||||
reverseWraparoundMode: false,
|
||||
sendFocusMode: false,
|
||||
synchronizedOutputMode: false,
|
||||
wraparoundMode: true
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,6 +124,7 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
originMode: m.origin,
|
||||
reverseWraparoundMode: m.reverseWraparound,
|
||||
sendFocusMode: m.sendFocus,
|
||||
synchronizedOutputMode: m.synchronizedOutput,
|
||||
wraparoundMode: m.wraparound
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
import { IImage32, decodePng } from '@lunapaint/png-codec';
|
||||
import { LocatorScreenshotOptions, test } from '@playwright/test';
|
||||
import { ITheme } from '@xterm/xterm';
|
||||
import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate } from './TestUtils';
|
||||
import { ITheme, type ITerminalOptions } from '@xterm/xterm';
|
||||
import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate, timeout } from './TestUtils';
|
||||
import { notDeepStrictEqual } from 'node:assert';
|
||||
|
||||
export interface ISharedRendererTestContext {
|
||||
value: ITestContext;
|
||||
@@ -1267,6 +1268,79 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('synchronized output', () => {
|
||||
test.beforeEach(async () => {
|
||||
const theme: ITheme = {
|
||||
background: '#000000FF',
|
||||
|
||||
red: '#FF0000FF',
|
||||
green: '#00FF00FF',
|
||||
blue: '#0000FFFF'
|
||||
};
|
||||
const options: ITerminalOptions = {}
|
||||
await ctx.value.page.evaluate(`
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.cursorStyle = 'underline';
|
||||
`);
|
||||
});
|
||||
test('defers rendering until ESU', async () => {
|
||||
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
|
||||
await ctx.value.proxy.write('\x1b[31m■');
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255], undefined, {
|
||||
equalityFn: (a, b) => {
|
||||
return !(a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]);
|
||||
}
|
||||
});
|
||||
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
|
||||
frameDetails = undefined;
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
|
||||
test('batches multiple writes', async () => {
|
||||
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
|
||||
await ctx.value.proxy.write('\x1b[31m■\x1b[32m■\x1b[34m■');
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255], undefined, {
|
||||
equalityFn: (a, b) => {
|
||||
return !(a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]);
|
||||
}
|
||||
});
|
||||
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
|
||||
frameDetails = undefined;
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 255, 0, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 255, 255]);
|
||||
});
|
||||
|
||||
test('nested BSU is idempotent', async () => {
|
||||
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
|
||||
await ctx.value.proxy.write('\x1b[31m■');
|
||||
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
|
||||
await ctx.value.proxy.write('\x1b[32m■');
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255], undefined, {
|
||||
equalityFn: (a, b) => {
|
||||
return !(a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]);
|
||||
}
|
||||
});
|
||||
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
|
||||
frameDetails = undefined;
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 255, 0, 255]);
|
||||
});
|
||||
|
||||
test('timeout flushes without ESU', async () => {
|
||||
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
|
||||
await ctx.value.proxy.write('\x1b[31m■');
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255], undefined, {
|
||||
equalityFn: (a, b) => {
|
||||
return !(a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]);
|
||||
}
|
||||
});
|
||||
await ctx.value.page.waitForTimeout(1000); // Timeout hard coded
|
||||
frameDetails = undefined;
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
enum CellColorPosition {
|
||||
@@ -1324,9 +1398,9 @@ export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestCont
|
||||
* @param col The 1-based column index to get the color for.
|
||||
* @param row The 1-based row index to get the color for.
|
||||
*/
|
||||
function getCellColor(ctx: ITestContext, col: number, row: number, position: CellColorPosition = CellColorPosition.CENTER): MaybeAsync<[red: number, green: number, blue: number, alpha: number]> {
|
||||
async function getCellColor(ctx: ITestContext, col: number, row: number, position: CellColorPosition = CellColorPosition.CENTER): Promise<[red: number, green: number, blue: number, alpha: number]> {
|
||||
if (!frameDetails) {
|
||||
return getFrameDetails(ctx).then(frameDetails => getCellColorInner(frameDetails, col, row));
|
||||
frameDetails = await getFrameDetails(ctx);
|
||||
}
|
||||
switch (position) {
|
||||
case CellColorPosition.CENTER:
|
||||
|
||||
@@ -609,6 +609,7 @@ test.describe('API Integration Tests', () => {
|
||||
originMode: false,
|
||||
reverseWraparoundMode: false,
|
||||
sendFocusMode: false,
|
||||
synchronizedOutputMode: false,
|
||||
wraparoundMode: true
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+7
@@ -1370,6 +1370,13 @@ declare module '@xterm/headless' {
|
||||
* Send FocusIn/FocusOut events: `CSI ? 1 0 0 4 h`
|
||||
*/
|
||||
readonly sendFocusMode: boolean;
|
||||
/**
|
||||
* Synchronized Output Mode: `CSI ? 2 0 2 6 h`
|
||||
*
|
||||
* When enabled, output is buffered and only rendered when the mode is
|
||||
* disabled, allowing for atomic screen updates without tearing.
|
||||
*/
|
||||
readonly synchronizedOutputMode: boolean;
|
||||
/**
|
||||
* Auto-Wrap Mode (DECAWM): `CSI ? 7 h`
|
||||
*/
|
||||
|
||||
Vendored
+7
@@ -1968,6 +1968,13 @@ declare module '@xterm/xterm' {
|
||||
* Send FocusIn/FocusOut events: `CSI ? 1 0 0 4 h`
|
||||
*/
|
||||
readonly sendFocusMode: boolean;
|
||||
/**
|
||||
* Synchronized Output Mode: `CSI ? 2 0 2 6 h`
|
||||
*
|
||||
* When enabled, output is buffered and only rendered when the mode is
|
||||
* disabled, allowing for atomic screen updates without tearing.
|
||||
*/
|
||||
readonly synchronizedOutputMode: boolean;
|
||||
/**
|
||||
* Auto-Wrap Mode (DECAWM): `CSI ? 7 h`
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user