Skip to content

Commit f1b0a96

Browse files
authored
Merge pull request #5770 from mgabor3141/fix/viewport-sync-output-flicker
fix: defer viewport DOM sync during synchronized output (DEC 2026)
2 parents 503abee + 49096a2 commit f1b0a96

File tree

2 files changed

+24
-2
lines changed

2 files changed

+24
-2
lines changed

demo/client/client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,8 +617,12 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
617617
}
618618

619619
function updateTerminalSize(): void {
620+
const showScrollbar = term!.options.scrollbar?.showScrollbar ?? true;
621+
const scrollBarWidth = (term!.options.scrollback === 0 || !showScrollbar)
622+
? 0
623+
: (term!.options.scrollbar?.width ?? 14);
620624
const width = optionsWindow.autoResize ? '100%'
621-
: ((term as any).dimensions.css.canvas.width + (term as any)._core.viewport.scrollBarWidth).toString() + 'px';
625+
: ((term as any).dimensions.css.canvas.width + scrollBarWidth).toString() + 'px';
622626
const height = optionsWindow.autoResize ? '100%'
623627
: ((term as any).dimensions.css.canvas.height).toString() + 'px';
624628
terminalContainer!.style.width = width;

src/browser/Viewport.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
77
import { ViewportConstants } from 'browser/shared/Constants';
88
import { Disposable, toDisposable } from 'common/Lifecycle';
9-
import { IBufferService, IMouseStateService, IOptionsService } from 'common/services/Services';
9+
import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from 'common/services/Services';
1010
import { CoreMouseEventType } from 'common/Types';
1111
import { scheduleAtNextAnimationFrame } from 'browser/Dom';
1212
import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement';
@@ -27,12 +27,14 @@ export class Viewport extends Disposable {
2727
private _isSyncing: boolean = false;
2828
private _isHandlingScroll: boolean = false;
2929
private _suppressOnScrollHandler: boolean = false;
30+
private _needsSyncOnRender: boolean = false;
3031

3132
constructor(
3233
element: HTMLElement,
3334
screenElement: HTMLElement,
3435
@IBufferService private readonly _bufferService: IBufferService,
3536
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
37+
@ICoreService private readonly _coreService: ICoreService,
3638
@IMouseStateService mouseStateService: IMouseStateService,
3739
@IThemeService themeService: IThemeService,
3840
@IOptionsService private readonly _optionsService: IOptionsService,
@@ -104,6 +106,16 @@ export class Viewport extends Disposable {
104106
}));
105107
this._register(this._bufferService.onScroll(() => this._sync()));
106108

109+
// Flush deferred viewport sync after a render completes (e.g. after ESU ends
110+
// synchronized output mode). This ensures DOM scroll position updates atomically
111+
// with the canvas render.
112+
this._register(this._renderService.onRender(() => {
113+
if (this._needsSyncOnRender) {
114+
this._needsSyncOnRender = false;
115+
this._sync();
116+
}
117+
}));
118+
107119
this._register(this._scrollableElement.onScroll(e => this._handleScroll(e)));
108120

109121
}
@@ -161,6 +173,12 @@ export class Viewport extends Disposable {
161173
if (!this._renderService || this._isSyncing) {
162174
return;
163175
}
176+
// Defer DOM scroll updates during synchronized output to prevent visible
177+
// scroll position flickering while the canvas content is frozen.
178+
if (this._coreService.decPrivateModes.synchronizedOutput) {
179+
this._needsSyncOnRender = true;
180+
return;
181+
}
164182
this._isSyncing = true;
165183

166184
// Ignore any onScroll event that happens as a result of dimensions changing as this should

0 commit comments

Comments
 (0)