Address PR review feedback for synchronized output

- Remove synchronizedOutputTimeout public API, hardcode 1s timeout
- Extract SynchronizedOutputHandler class for cleaner code
- Update spec URL to contour-terminal/vt-extensions
- Remove unnecessary comment in InputHandler
- Delete unit tests, add integration tests in SharedRendererTests
- Fix whitespace issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Lloyd
2025-12-09 07:58:31 -08:00
co-authored by Claude
parent 7228e2e2dc
commit 6b63c5f260
7 changed files with 114 additions and 560 deletions
-495
View File
@@ -1,495 +0,0 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import jsdom = require('jsdom');
import { RenderService } from 'browser/services/RenderService';
import { MockBufferService, MockCoreService, MockOptionsService } from 'common/TestUtils.test';
import { IRenderer, IRenderDimensions } from 'browser/renderer/shared/Types';
import { ICoreBrowserService } from 'browser/services/Services';
// Test timing constants
const RENDER_DEBOUNCE_DELAY = 50; // Time to wait for debounced renders
const DEFAULT_SYNC_OUTPUT_TIMEOUT = 5000; // Default synchronized output timeout
const TIMEOUT_TEST_BUFFER = 500; // Extra time to wait in timeout tests
class MockRenderer implements IRenderer {
public renderRowsCalls: Array<{ start: number; end: number }> = [];
public dimensions: IRenderDimensions = {
device: {
char: { width: 10, height: 20, left: 0, top: 0 },
cell: { width: 10, height: 20 },
canvas: { width: 800, height: 600 }
},
css: {
canvas: { width: 800, height: 600 },
cell: { width: 10, height: 20 }
}
};
renderRows(start: number, end: number): void {
this.renderRowsCalls.push({ start, end });
}
onRequestRedraw(listener: (e: { start: number; end: number }) => void): { dispose: () => void } {
return { dispose: () => { } };
}
clearCells(x: number, y: number, width: number, height: number): void { }
clearTextureAtlas(): void { }
clear(): void { }
handleDevicePixelRatioChange(): void { }
handleResize(cols: number, rows: number): void { }
handleCharSizeChanged(): void { }
handleBlur(): void { }
handleFocus(): void { }
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { }
handleCursorMove(): void { }
handleOptionsChanged(): void { }
dispose(): void { }
}
class MockCoreBrowserService implements ICoreBrowserService {
public serviceBrand: any;
public isFocused: boolean = true;
public window: any;
public mainDocument: Document;
public onDprChange = () => ({ dispose: () => { } });
public onWindowChange = () => ({ dispose: () => { } });
public get dpr(): number { return 1; }
constructor(window: Window) {
this.window = window;
this.mainDocument = window.document;
// Add requestAnimationFrame and cancelAnimationFrame if not present
if (!this.window.requestAnimationFrame) {
this.window.requestAnimationFrame = (callback: FrameRequestCallback) => {
return setTimeout(() => callback(Date.now()), 0) as any;
};
this.window.cancelAnimationFrame = (id: number) => {
clearTimeout(id);
};
}
}
}
class MockCharSizeService {
public serviceBrand: any;
public width: number = 10;
public height: number = 20;
public hasValidSize: boolean = true;
public onCharSizeChange = () => ({ dispose: () => { } });
public measure(): void { }
}
class MockDecorationService {
public serviceBrand: any;
public decorations: any[] = [];
public onDecorationRegistered = () => ({ dispose: () => { } });
public onDecorationRemoved = () => ({ dispose: () => { } });
}
class MockThemeService {
public serviceBrand: any;
public colors: any = {};
public onChangeColors = () => ({ dispose: () => { } });
}
describe('RenderService', () => {
let dom: jsdom.JSDOM;
let window: Window;
let renderService: RenderService;
let mockRenderer: MockRenderer;
let coreService: MockCoreService;
let bufferService: MockBufferService;
let coreBrowserService: MockCoreBrowserService;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window as any as Window;
const screenElement = window.document.createElement('div');
coreService = new MockCoreService();
bufferService = new MockBufferService(80, 30);
coreBrowserService = new MockCoreBrowserService(window);
renderService = new RenderService(
30,
screenElement,
new MockOptionsService() as any,
new MockCharSizeService() as any,
coreService as any,
new MockDecorationService() as any,
bufferService as any,
coreBrowserService as any,
new MockThemeService() as any
);
mockRenderer = new MockRenderer();
renderService.setRenderer(mockRenderer);
});
afterEach(() => {
renderService.dispose();
});
describe('synchronized output mode', () => {
it('should defer rendering when synchronized output is enabled', (done) => {
// Clear any initial renders from setRenderer
mockRenderer.renderRowsCalls = [];
// Enable synchronized output
coreService.decPrivateModes.synchronizedOutput = true;
// Request a refresh
renderService.refreshRows(0, 10);
// Give time for the debounced render to trigger
setTimeout(() => {
// Renderer should NOT have been called
assert.equal(mockRenderer.renderRowsCalls.length, 0, 'Renderer should not be called during synchronized output');
done();
}, RENDER_DEBOUNCE_DELAY);
});
it('should flush buffered rows when synchronized output is disabled', (done) => {
// Clear any initial renders from setRenderer
mockRenderer.renderRowsCalls = [];
// Enable synchronized output
coreService.decPrivateModes.synchronizedOutput = true;
// Request multiple refreshes while in synchronized mode
renderService.refreshRows(0, 5);
renderService.refreshRows(10, 15);
renderService.refreshRows(3, 20);
setTimeout(() => {
// Verify no renders happened yet
assert.equal(mockRenderer.renderRowsCalls.length, 0);
// Disable synchronized output
coreService.decPrivateModes.synchronizedOutput = false;
// Request a refresh to trigger the flush
renderService.refreshRows(0, 0);
setTimeout(() => {
// Should have rendered the accumulated range
// Note: The test triggers with refreshRows(0, 0), but the accumulated buffer may extend further
assert.equal(mockRenderer.renderRowsCalls.length, 1, 'Should render once after disabling synchronized output');
const call = mockRenderer.renderRowsCalls[0];
assert.equal(call.start, 0, 'Should render from start of accumulated range');
// The accumulated range should include all requested rows (0-5, 10-15, 3-20 = 0-20)
assert.isAtLeast(call.end, 15, 'Should render at least to row 15');
done();
}, 50);
}, 50);
});
it('should render normally when synchronized output is not enabled', (done) => {
// Wait for any pending renders from initialization
setTimeout(() => {
// Clear any initial renders from setRenderer
mockRenderer.renderRowsCalls = [];
// Synchronized output is disabled by default
assert.equal(coreService.decPrivateModes.synchronizedOutput, false);
// Request a refresh
renderService.refreshRows(5, 10);
setTimeout(() => {
// Renderer SHOULD have been called
assert.equal(mockRenderer.renderRowsCalls.length, 1);
assert.equal(mockRenderer.renderRowsCalls[0].start, 5);
assert.equal(mockRenderer.renderRowsCalls[0].end, 10);
done();
}, 50);
}, 50);
});
it('should accumulate row ranges correctly', (done) => {
// Clear any initial renders from setRenderer
mockRenderer.renderRowsCalls = [];
coreService.decPrivateModes.synchronizedOutput = true;
// Multiple non-overlapping ranges
renderService.refreshRows(5, 10);
renderService.refreshRows(20, 25);
renderService.refreshRows(0, 3);
setTimeout(() => {
assert.equal(mockRenderer.renderRowsCalls.length, 0);
// Disable and flush
coreService.decPrivateModes.synchronizedOutput = false;
renderService.refreshRows(0, 0);
setTimeout(() => {
assert.equal(mockRenderer.renderRowsCalls.length, 1);
// Should accumulate min to max: 0 to 25 (or full viewport if refresh triggered full update)
assert.equal(mockRenderer.renderRowsCalls[0].start, 0);
assert.isAtLeast(mockRenderer.renderRowsCalls[0].end, 25, 'Should render at least to row 25');
done();
}, 50);
}, 50);
});
it('should handle timeout and force render', function(done) {
// This test needs more time for the timeout
this.timeout(10000);
// Clear any initial renders from setRenderer
mockRenderer.renderRowsCalls = [];
coreService.decPrivateModes.synchronizedOutput = true;
// Request a refresh
renderService.refreshRows(0, 10);
setTimeout(() => {
// Should not have rendered yet
assert.equal(mockRenderer.renderRowsCalls.length, 0);
}, 100);
// Wait for timeout (default timeout + buffer)
setTimeout(() => {
// Timeout should have forced a render
assert.equal(mockRenderer.renderRowsCalls.length, 1, 'Timeout should force render');
assert.equal(mockRenderer.renderRowsCalls[0].start, 0);
assert.isAtLeast(mockRenderer.renderRowsCalls[0].end, 10, 'Should render at least the requested rows');
// Mode should have been automatically disabled
assert.equal(coreService.decPrivateModes.synchronizedOutput, false, 'Timeout should disable synchronized output');
done();
}, DEFAULT_SYNC_OUTPUT_TIMEOUT + TIMEOUT_TEST_BUFFER);
});
it('should restart timeout on each buffered render request', function(done) {
this.timeout(12000);
// Clear any initial renders from setRenderer
mockRenderer.renderRowsCalls = [];
coreService.decPrivateModes.synchronizedOutput = true;
// First request
renderService.refreshRows(0, 5);
// Keep requesting refreshes every 2 seconds (before 5s timeout)
let requestCount = 0;
const interval = setInterval(() => {
requestCount++;
renderService.refreshRows(0, 5);
if (requestCount >= 2) {
clearInterval(interval);
// After stopping requests, wait for timeout
setTimeout(() => {
// Should have rendered after final timeout
assert.equal(mockRenderer.renderRowsCalls.length, 1, 'Should render after final timeout');
assert.equal(coreService.decPrivateModes.synchronizedOutput, false);
done();
}, 5500);
}
}, 2000);
});
it('should clear buffered state after flush', (done) => {
// Clear any initial renders from setRenderer
mockRenderer.renderRowsCalls = [];
coreService.decPrivateModes.synchronizedOutput = true;
// First cycle
renderService.refreshRows(0, 10);
setTimeout(() => {
coreService.decPrivateModes.synchronizedOutput = false;
renderService.refreshRows(0, 0);
setTimeout(() => {
assert.equal(mockRenderer.renderRowsCalls.length, 1);
mockRenderer.renderRowsCalls = [];
// Second cycle - should not include rows from first cycle
coreService.decPrivateModes.synchronizedOutput = true;
renderService.refreshRows(20, 25);
setTimeout(() => {
coreService.decPrivateModes.synchronizedOutput = false;
renderService.refreshRows(0, 0);
setTimeout(() => {
assert.equal(mockRenderer.renderRowsCalls.length, 1);
assert.equal(mockRenderer.renderRowsCalls[0].start, 20);
assert.equal(mockRenderer.renderRowsCalls[0].end, 25);
done();
}, 50);
}, 50);
}, 50);
}, 50);
});
it('should handle BSU sent twice without ESU (idempotent)', (done) => {
// Clear any initial renders
mockRenderer.renderRowsCalls = [];
// Enable synchronized output
coreService.decPrivateModes.synchronizedOutput = true;
renderService.refreshRows(0, 10);
setTimeout(() => {
assert.equal(mockRenderer.renderRowsCalls.length, 0);
// Enable again (should be idempotent)
coreService.decPrivateModes.synchronizedOutput = true;
renderService.refreshRows(10, 20);
setTimeout(() => {
// Still no rendering
assert.equal(mockRenderer.renderRowsCalls.length, 0);
// Now disable
coreService.decPrivateModes.synchronizedOutput = false;
renderService.refreshRows(0, 0);
setTimeout(() => {
// Should render accumulated range
assert.equal(mockRenderer.renderRowsCalls.length, 1);
assert.equal(mockRenderer.renderRowsCalls[0].start, 0);
assert.isAtLeast(mockRenderer.renderRowsCalls[0].end, 20);
done();
}, 50);
}, 50);
}, 50);
});
it('should handle ESU without BSU (no-op)', (done) => {
// Wait for any pending renders
setTimeout(() => {
// Clear any initial renders
mockRenderer.renderRowsCalls = [];
// Synchronized output is already disabled (default state)
assert.equal(coreService.decPrivateModes.synchronizedOutput, false);
// Disable again (ESU without BSU)
coreService.decPrivateModes.synchronizedOutput = false;
renderService.refreshRows(5, 10);
setTimeout(() => {
// Should render - ESU without BSU should not cause issues
// The exact rows may vary due to previous test state, but rendering should occur
assert.isAtLeast(mockRenderer.renderRowsCalls.length, 1, 'Should render even with ESU before BSU');
done();
}, RENDER_DEBOUNCE_DELAY);
}, RENDER_DEBOUNCE_DELAY);
});
it('should handle rapid enable/disable toggling', (done) => {
// Clear any initial renders
mockRenderer.renderRowsCalls = [];
// Rapid toggling
coreService.decPrivateModes.synchronizedOutput = true;
renderService.refreshRows(0, 5);
setTimeout(() => {
coreService.decPrivateModes.synchronizedOutput = false;
renderService.refreshRows(0, 0);
setTimeout(() => {
const firstRenderCount = mockRenderer.renderRowsCalls.length;
// Toggle again immediately
coreService.decPrivateModes.synchronizedOutput = true;
renderService.refreshRows(10, 15);
setTimeout(() => {
coreService.decPrivateModes.synchronizedOutput = false;
renderService.refreshRows(0, 0);
setTimeout(() => {
// Should have rendered both cycles
assert.isAtLeast(mockRenderer.renderRowsCalls.length, firstRenderCount + 1);
done();
}, 50);
}, 50);
}, 50);
}, 50);
});
it('should handle terminal resize during synchronized output', (done) => {
// Clear any initial renders
mockRenderer.renderRowsCalls = [];
coreService.decPrivateModes.synchronizedOutput = true;
renderService.refreshRows(0, 10);
setTimeout(() => {
// Resize terminal
renderService.resize(80, 50);
// Continue buffering with new size
renderService.refreshRows(40, 45);
setTimeout(() => {
// Disable synchronized output
coreService.decPrivateModes.synchronizedOutput = false;
renderService.refreshRows(0, 0);
setTimeout(() => {
// Should have rendered (clamped to new size if needed)
assert.isAtLeast(mockRenderer.renderRowsCalls.length, 1);
done();
}, 50);
}, 50);
}, 50);
});
it('should not timeout when timeout is disabled', function(done) {
this.timeout(3000);
// Create a new render service with timeout disabled
const optionsServiceWithTimeout = new MockOptionsService({ synchronizedOutputTimeout: 0 }) as any;
const newRenderService = new RenderService(
30,
window.document.createElement('div'),
optionsServiceWithTimeout,
new MockCharSizeService() as any,
coreService as any,
new MockDecorationService() as any,
bufferService as any,
coreBrowserService as any,
new MockThemeService() as any
);
const newMockRenderer = new MockRenderer();
newRenderService.setRenderer(newMockRenderer);
setTimeout(() => {
newMockRenderer.renderRowsCalls = [];
coreService.decPrivateModes.synchronizedOutput = true;
newRenderService.refreshRows(0, 10);
// Wait longer than the default timeout would be
setTimeout(() => {
// Should NOT have rendered (timeout disabled)
assert.equal(newMockRenderer.renderRowsCalls.length, 0);
// Mode should still be enabled
assert.equal(coreService.decPrivateModes.synchronizedOutput, true);
newRenderService.dispose();
done();
}, 1000);
}, 50);
});
});
});
+73 -51
View File
@@ -18,6 +18,66 @@ interface ISelectionState {
columnSelectMode: boolean;
}
const SYNCHRONIZED_OUTPUT_TIMEOUT_MS = 1000;
/**
* 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();
}, 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;
}
}
}
export class RenderService extends Disposable implements IRenderService {
public serviceBrand: undefined;
@@ -33,9 +93,7 @@ export class RenderService extends Disposable implements IRenderService {
private _needsSelectionRefresh: boolean = false;
private _canvasWidth: number = 0;
private _canvasHeight: number = 0;
private _synchronizedOutputTimeout: number | undefined;
private _synchronizedOutputStart: number = 0;
private _synchronizedOutputEnd: number = 0;
private _syncOutputHandler: SynchronizedOutputHandler;
private _selectionState: ISelectionState = {
start: undefined,
end: undefined,
@@ -69,12 +127,12 @@ export class RenderService extends Disposable implements IRenderService {
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), this._coreBrowserService);
this._register(this._renderDebouncer);
// Clear synchronized output timeout on dispose
this._register(toDisposable(() => {
if (this._synchronizedOutputTimeout !== undefined) {
this._coreBrowserService.window.clearTimeout(this._synchronizedOutputTimeout);
}
}));
this._syncOutputHandler = new SynchronizedOutputHandler(
this._coreBrowserService,
this._coreService,
() => this._fullRefresh()
);
this._register(toDisposable(() => this._syncOutputHandler.dispose()));
this._register(this._coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
@@ -150,43 +208,15 @@ export class RenderService extends Disposable implements IRenderService {
return;
}
// Handle synchronized output mode (DEC 2026)
if (this._coreService.decPrivateModes.synchronizedOutput) {
// Track the row range that needs refreshing
if (!this._needsFullRefresh) {
// First request in this sync cycle
this._synchronizedOutputStart = start;
this._synchronizedOutputEnd = end;
this._needsFullRefresh = true;
} else {
// Expand the tracked range to include new rows
this._synchronizedOutputStart = Math.min(this._synchronizedOutputStart, start);
this._synchronizedOutputEnd = Math.max(this._synchronizedOutputEnd, end);
}
// Start a safety timeout if not already running and timeout is enabled
const timeout = this._optionsService.options.synchronizedOutputTimeout;
if (this._synchronizedOutputTimeout === undefined && timeout && timeout > 0) {
this._synchronizedOutputTimeout = this._coreBrowserService.window.setTimeout(() => {
this._synchronizedOutputTimeout = undefined;
// Force-disable the mode and trigger a refresh
this._coreService.decPrivateModes.synchronizedOutput = false;
this._fullRefresh();
}, timeout);
}
this._syncOutputHandler.bufferRows(start, end);
return;
}
// Clear the timeout if synchronized output mode was just disabled
if (this._synchronizedOutputTimeout !== undefined) {
this._coreBrowserService.window.clearTimeout(this._synchronizedOutputTimeout);
this._synchronizedOutputTimeout = undefined;
}
// If we were in synchronized output mode, use the tracked row range
if (this._needsFullRefresh) {
start = this._synchronizedOutputStart;
end = this._synchronizedOutputEnd;
this._needsFullRefresh = false;
const buffered = this._syncOutputHandler.flush();
if (buffered) {
start = Math.min(start, buffered.start);
end = Math.max(end, buffered.end);
}
if (!isRedrawOnly) {
@@ -203,15 +233,7 @@ export class RenderService extends Disposable implements IRenderService {
// 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) {
// Track the row range that needs refreshing
if (!this._needsFullRefresh) {
this._synchronizedOutputStart = start;
this._synchronizedOutputEnd = end;
this._needsFullRefresh = true;
} else {
this._synchronizedOutputStart = Math.min(this._synchronizedOutputStart, start);
this._synchronizedOutputEnd = Math.max(this._synchronizedOutputEnd, end);
}
this._syncOutputHandler.bufferRows(start, end);
return;
}
+2 -3
View File
@@ -1969,7 +1969,7 @@ 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://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036)
case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/main/synchronized-output.md)
this._coreService.decPrivateModes.synchronizedOutput = true;
break;
}
@@ -2200,9 +2200,8 @@ 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://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036)
case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/main/synchronized-output.md)
this._coreService.decPrivateModes.synchronizedOutput = false;
// Trigger a full refresh now that the synchronized output block has ended
this._onRequestRefreshRows.fire(undefined);
break;
}
-1
View File
@@ -37,7 +37,6 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
scrollSensitivity: 1,
screenReaderMode: false,
smoothScrollDuration: 0,
synchronizedOutputTimeout: 5000,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
minimumContrastRatio: 1,
-1
View File
@@ -259,7 +259,6 @@ export interface ITerminalOptions {
scrollOnUserInput?: boolean;
scrollSensitivity?: number;
smoothScrollDuration?: number;
synchronizedOutputTimeout?: number;
tabStopWidth?: number;
theme?: ITheme;
windowsMode?: boolean;
+39
View File
@@ -1267,6 +1267,45 @@ 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('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), [0, 0, 0, 255]);
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [205, 49, 49, 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), [0, 0, 0, 255]);
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [205, 49, 49, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [13, 188, 121, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [36, 114, 200, 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), [0, 0, 0, 255]);
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [205, 49, 49, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [13, 188, 121, 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), [0, 0, 0, 255]);
await ctx.value.page.waitForTimeout(1500);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [205, 49, 49, 255]);
});
});
}
enum CellColorPosition {
-9
View File
@@ -281,15 +281,6 @@ declare module '@xterm/xterm' {
*/
smoothScrollDuration?: number;
/**
* The timeout in milliseconds for synchronized output mode (DEC mode 2026).
* When an application enables synchronized output but fails to disable it
* within this timeout, the terminal will automatically flush buffered
* output to prevent the display from freezing indefinitely. Set to 0 to
* disable the timeout (not recommended). The default is 5000 (5 seconds).
*/
synchronizedOutputTimeout?: number;
/**
* The size of tab stops in the terminal.
*/