Summary
On @xterm/xterm@6.1.0-beta.302 + @xterm/addon-webgl@0.20.0-beta.298, the cursor blink
never starts in a headless X (Xvfb) Electron environment, even though cursorBlink: true
and the WebGL renderer is active. Downgrading only the xterm packages to
@xterm/xterm@6.0.0 + @xterm/addon-webgl@0.19.0 — same app, same machine, same Xvfb
display — makes it blink normally.
The same beta build does blink when the app runs on a real X display, so this looks
environment-sensitive rather than unconditionally broken.
Environment
- Electron 43.1.0 (Chromium 150), Linux x64
- Headless X via
xvfb-run, software GL (SwiftShader)
- Terminal created with
new Terminal({ cursorBlink: true, ... }), then loadAddon(new WebglAddon())
- Renderer confirmed to be the WebGL one (link-layer canvas present, no
.xterm-rows)
Evidence
We instrumented the renderer by wrapping window.setInterval / window.clearInterval /
window.setTimeout before creating any terminal, then counted live intervals and scheduled
timeouts by delay.
0.19.0 / 6.0.0 (same environment):
live intervals: [600] ← blink interval created
scheduled timeouts (top): [[2000,4],[0,2],[600,2],[60,1],[206,1]]
requestAnimationFrame rate: ~3.3/s
0.20.0-beta.298 / 6.1.0-beta.302 (same environment, only versions changed):
live intervals: [] ← no blink interval, ever
scheduled timeouts (top): [[0,4],[2000,3],[300000,1],[60,1],[15000,1]]
requestAnimationFrame rate: 0.0/s
At the moment of measurement:
.xterm classList: ["terminal","xterm","focus"]
document.hasFocus(): true
document.activeElement: textarea.xterm-helper-textarea
The detail that seems most diagnostic
In CursorBlinkStateManager's constructor the two calls are in the same isFocused &&
branch:
this._coreBrowserService.isFocused && (this._restartInterval(), this._resetIdleTimer());
On the beta we observe 300000 scheduled exactly once (the
CURSOR_BLINK_IDLE_TIMEOUT from _resetIdleTimer), but no 600 timeout at all —
i.e. the branch clearly ran, yet _restartInterval() never scheduled its
_blinkStartTimeout.
What we ruled out (by diffing 6.0.0 vs 6.1.0-beta.302 and 0.19.0 vs 0.20.0-beta.298)
decPrivateModes.cursorBlink default — void 0 in both, so ?? options.cursorBlink applies
get isFocused() — byte-identical between core versions
_restartInterval() — same shape in both addon versions (setTimeout(..., timeToStart = 600))
prefers-reduced-motion / visibilityState — not referenced by either version
- The new
TextBlinkStateManager viewport gating (_intervalDuration > 0 && _needsBlinkInViewport && _isViewportVisible) — that is the SGR blink attribute and is off by default
(blinkIntervalDuration: 0)
Suspicion
#5523 ("Add idle timeout which pauses cursor blink", merged 2025-12-28) is the only change to
CursorBlinkStateManager after the 6.0.0 / 0.19.0 release (2025-12-22), and it is the source of
the 300000 timer we observe. We could not identify the exact line that suppresses the start,
so this is a suspicion, not a conclusion.
Caveat
We have not reduced this to a standalone minimal repro outside our Electron app — the above is
from an automated end-to-end test of our own application, where the only variable changed
between the two runs was the xterm package versions (plus a rebuild).
Happy to run further instrumented experiments in this environment if that would help narrow it
down.
Summary
On
@xterm/xterm@6.1.0-beta.302+@xterm/addon-webgl@0.20.0-beta.298, the cursor blinknever starts in a headless X (Xvfb) Electron environment, even though
cursorBlink: trueand the WebGL renderer is active. Downgrading only the xterm packages to
@xterm/xterm@6.0.0+@xterm/addon-webgl@0.19.0— same app, same machine, same Xvfbdisplay — makes it blink normally.
The same beta build does blink when the app runs on a real X display, so this looks
environment-sensitive rather than unconditionally broken.
Environment
xvfb-run, software GL (SwiftShader)new Terminal({ cursorBlink: true, ... }), thenloadAddon(new WebglAddon()).xterm-rows)Evidence
We instrumented the renderer by wrapping
window.setInterval/window.clearInterval/window.setTimeoutbefore creating any terminal, then counted live intervals and scheduledtimeouts by delay.
0.19.0 / 6.0.0 (same environment):
0.20.0-beta.298 / 6.1.0-beta.302 (same environment, only versions changed):
At the moment of measurement:
The detail that seems most diagnostic
In
CursorBlinkStateManager's constructor the two calls are in the sameisFocused &&branch:
On the beta we observe
300000scheduled exactly once (theCURSOR_BLINK_IDLE_TIMEOUTfrom_resetIdleTimer), but no600timeout at all —i.e. the branch clearly ran, yet
_restartInterval()never scheduled its_blinkStartTimeout.What we ruled out (by diffing 6.0.0 vs 6.1.0-beta.302 and 0.19.0 vs 0.20.0-beta.298)
decPrivateModes.cursorBlinkdefault —void 0in both, so?? options.cursorBlinkappliesget isFocused()— byte-identical between core versions_restartInterval()— same shape in both addon versions (setTimeout(..., timeToStart = 600))prefers-reduced-motion/visibilityState— not referenced by either versionTextBlinkStateManagerviewport gating (_intervalDuration > 0 && _needsBlinkInViewport && _isViewportVisible) — that is the SGR blink attribute and is off by default(
blinkIntervalDuration: 0)Suspicion
#5523 ("Add idle timeout which pauses cursor blink", merged 2025-12-28) is the only change to
CursorBlinkStateManagerafter the 6.0.0 / 0.19.0 release (2025-12-22), and it is the source ofthe
300000timer we observe. We could not identify the exact line that suppresses the start,so this is a suspicion, not a conclusion.
Caveat
We have not reduced this to a standalone minimal repro outside our Electron app — the above is
from an automated end-to-end test of our own application, where the only variable changed
between the two runs was the xterm package versions (plus a rebuild).
Happy to run further instrumented experiments in this environment if that would help narrow it
down.