Skip to content

v2: tea.Repaint to recover after external tty damage (ClearScreen is not enough) #1808

Description

@florianleibert

Problem

tea.ClearScreen (added for #236) erases the renderer cell buffer, but it does not re-enter alternate-screen / input / cursor modes. Combined with the unchanged-view skip in cursedRenderer.flush, an app cannot recover when something outside the renderer mutates the tty.

Reproduced against v2.0.5 (and the exported API is unchanged through v2.0.9):

  1. Program is in the alternate screen with a stable frame.
  2. A child (or any other writer) emits ESC[?1049l. The physical terminal is now on the primary screen; the renderer still believes its full alt-screen frame is present.
  3. The next content change paints only the cell delta (a few bytes). No 1049h.
  4. tea.ClearScreen with a byte-identical view writes nothing on a live tick (flush returns early because !starting && viewEquals(lastView, view)).
  5. The next changed frame after ClearScreen redraws text, still without re-entering the alternate screen.

Program.RestoreTerminal already proves the repair: renderer.start() replays modes from lastView, sets starting = true so the skip cannot fire, then flush emits a full frame. There is no in-loop equivalent that does this without leaving the alternate screen.

This is the same failure class #236 pointed at: a tool writes to the tty, the display is corrupt, and the app needs a forced redraw. ClearScreen never re-acquired modes, so it did not close that case.

Proposed API

func Repaint() Msg

Handled next to clearScreenMsg. The renderer, in one locked transaction:

  1. reset() — drop cell-diff assumptions (fresh uv.TerminalRenderer)
  2. replay modes from lastView (the body of start(), extracted so it can run under the existing lock)
  3. set starting = true so the next flush cannot skip an unchanged view
  4. MoveTo(0, 0) + Erase() — a fresh renderer thinks the screen is blank, so cells that are blank in our frame would otherwise leave stale content in place

A nil lastView (very early startup) is a no-op on modes plus erase.

This is not implemented by toggling View.AltScreen or writing escapes around the renderer: both leave the cell cache wrong.

ClearScreen stays as-is for wiping visual clutter in inline mode. Repaint is the recovery operation.

Tests

  • ClearScreen + identical alt-screen view does not emit ESC[?1049h (the neutralized-fix baseline).
  • Repaint + identical alt-screen view does re-enter the alternate screen and redraw the content.
  • Proof is against renderer output bytes, not Model.View(). Send+Quit cannot prove this: flush(closing=true) already bypasses the skip.

Happy to send a PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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