Add synchronized output support (DEC mode 2026)

Implement synchronized output mode (CSI ? 2026 h/l) which allows
applications to batch terminal updates and render them atomically,
preventing screen tearing during rapid output.

Features:
- BSU (CSI ? 2026 h) pauses rendering, buffering row updates
- ESU (CSI ? 2026 l) flushes buffer and renders atomically
- Configurable timeout via synchronizedOutputTimeout option (default 5s)
- Exposed via terminal.modes.synchronizedOutputMode

Closes #3375

🤖 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-04 11:16:25 -08:00
co-authored by Claude
parent 46c510a76d
commit 7228e2e2dc
10 changed files with 602 additions and 10 deletions
+1
View File
@@ -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
};
}
+495
View File
@@ -0,0 +1,495 @@
/**
* 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);
});
});
});
+77 -10
View File
@@ -9,7 +9,7 @@ import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { Disposable, MutableDisposable, toDisposable } from '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,7 @@ interface ISelectionState {
columnSelectMode: boolean;
}
export class RenderService extends Disposable implements IRenderService {
public serviceBrand: undefined;
@@ -32,6 +33,9 @@ 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 _selectionState: ISelectionState = {
start: undefined,
end: undefined,
@@ -52,23 +56,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()));
// Clear synchronized output timeout on dispose
this._register(toDisposable(() => {
if (this._synchronizedOutputTimeout !== undefined) {
this._coreBrowserService.window.clearTimeout(this._synchronizedOutputTimeout);
}
}));
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 +90,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 +108,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 +149,46 @@ export class RenderService extends Disposable implements IRenderService {
this._needsFullRefresh = true;
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);
}
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;
}
if (!isRedrawOnly) {
this._isNextRenderRedrawOnly = false;
}
@@ -148,6 +200,21 @@ 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) {
// 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);
}
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.
+8
View File
@@ -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://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036)
this._coreService.decPrivateModes.synchronizedOutput = true;
break;
}
}
return true;
@@ -2197,6 +2200,11 @@ 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)
this._coreService.decPrivateModes.synchronizedOutput = false;
// Trigger a full refresh now that the synchronized output block has ended
this._onRequestRefreshRows.fire(undefined);
break;
}
}
return true;
+1
View File
@@ -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;
+1
View File
@@ -273,6 +273,7 @@ export interface IDecPrivateModes {
origin: boolean;
reverseWraparound: boolean;
sendFocus: boolean;
synchronizedOutput: boolean;
wraparound: boolean; // defaults: xterm - true, vt100 - false
}
+1
View File
@@ -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
});
+1
View File
@@ -37,6 +37,7 @@ 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,6 +259,7 @@ export interface ITerminalOptions {
scrollOnUserInput?: boolean;
scrollSensitivity?: number;
smoothScrollDuration?: number;
synchronizedOutputTimeout?: number;
tabStopWidth?: number;
theme?: ITheme;
windowsMode?: boolean;
+16
View File
@@ -281,6 +281,15 @@ 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.
*/
@@ -1968,6 +1977,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`
*/