Skip to content

TerminalUI.display() resets JLine's Display every redraw, defeating frame diffing (full-screen rewrite per frame) #1361

Description

@Benjamin-Tomkins

Summary

In the JLine TUI layer, TerminalUI.display() calls display.clear() + display.reset() on every redraw in the fullScreen path, immediately before display.update(newLines, …). org.jline.utils.Display.reset() discards the cached previous frame, so update() has no diff base and rewrites the entire screen on every frame instead of only the changed cells. JLine's cell-level diffing is effectively disabled for every redraw.

This is invisible for static frames (the redundant bytes render identical pixels), but very visible during a fast window resize, where each redraw re-lays out content: every frame becomes a full, non-atomic screen rewrite, producing tearing/flicker on terminals without synchronized output (e.g. macOS Terminal.app).

Environment

  • spring-shell-jline 4.0.3-SNAPSHOT
  • JLine 3.30.x (FFM PTY provider, Java 25)
  • macOS Terminal.app (no DECSET 2026 synchronized output)

Evidence

org/springframework/shell/jline/tui/component/view/TerminalUI.java, display() (the body invoked for every redraw()):

private synchronized void display() {
    ...
    if (fullScreen) {
        display.clear();
        display.reset();   // wipes JLine's previous-frame cache (the diff base)
        display.resize(size.getRows(), size.getColumns());
        ...
        render(rect);
    }
    ...
    display.update(newLines, targetCursorPos);  // would cell-diff, but reset() just nulled its base
}

Because reset() runs before update() on every frame, update() always performs a full redraw.

Impact

  • Full-screen rewrite per frame regardless of how little changed.
  • Fast resize: tearing / blank-growth / flicker on terminals lacking synchronized output.
  • Higher TTY write volume than necessary at any repaint cadence.

Proposed fix

  1. Only clear()/reset()/resize() when the terminal size actually changed (track the last applied size); for same-size redraws, go straight to render(rect) + display.update(...) and let JLine diff. Keeps resize correctness while restoring incremental updates for the common case.
  2. Optionally emit synchronized output (DECSET 2026 / BSU-ESU) around the write when the terminal advertises it, so even a full rewrite is presented atomically.

(1) is the higher-value change — it makes both the steady-state and resize paths write only what changed.

Notes

Found while diagnosing resize tearing in a downstream Compose-style TUI built on spring-shell-jline. Consumer-side fixes (view-rect sync, resize-event coalescing) reduce the symptom but cannot remove the per-frame full rewrite, which lives in TerminalUI.display().

Metadata

Metadata

Assignees

No one assigned

    Labels

    status/need-triageTeam needs to triage and take a first look

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions