Skip to content

Cursor blink never starts on 6.1.0-beta in a headless X (Xvfb) Electron environment — works on 6.0.0 in the same environment #6113

Description

@kewang

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions