Skip to content

Support typed resize recovery outside the retained live frame #56

Description

@zikolach

Problem or use case

A normal-screen interactive console can have two different kinds of output:

  1. durable, append-only history published with TUI.appendToScrollback; and
  2. one retained live frame containing mutable state such as status, an editor draft, and a footer.

With NormalResizeClearPolicy.PreserveScrollback, a resize preserves terminal scrollback but clears the active viewport before redrawing the retained frame. An application may keep a bounded semantic transcript so it can reflow and restore durable text that was in the invalidated viewport. However, Siglyph currently has no typed, owner-serialized place to publish that recovery output.

Putting recovered history into the retained component tree is not correct:

  • the next appendToScrollback operation is inserted above the retained frame, so chronology becomes new output → recovered old output;
  • replaying the complete transcript duplicates entries that already remain in preserved scrollback;
  • recovered durable content becomes part of ordinary retained-frame redraws instead of remaining detached, one-shot output.

Writing directly to the terminal would bypass Siglyph's serialization, typed-control validation, cursor bookkeeping, and cleanup ownership.

Proposed solution

Add a typed normal-screen resize-recovery boundary owned by TUI. Its purpose would be to reconstruct only the durable tail lost from the active viewport, immediately before the retained live frame, without making that tail part of previousFrame.

One possible API shape is:

trait NormalResizeRecoveryProvider:
  def render(context: NormalResizeRecoveryContext): ComponentRender

final case class NormalResizeRecoveryContext(
    width: Int,
    height: Int,
    maxRows: Int
)

TUIOptions(
  normalResizeClearPolicy = NormalResizeClearPolicy.PreserveScrollback,
  normalResizeRecovery = Some(provider)
)

The exact naming and registration mechanism are open, but suggested semantics are:

  1. Invoke the provider only for a geometry-changing normal-screen resize, not initial render, forced render, ordinary differential redraw, or append publication.
  2. Render the retained live frame first, then calculate a strict recovery budget such as max(0, terminalHeight - visibleLiveFrameRows).
  3. Ask the provider for current-width semantic output bounded to that row budget. The application can return the newest retained rows needed to reconstruct the invalidated viewport rather than replaying all history.
  4. After the viewport clear, serialize recovery output followed by the retained live frame in one TUI-owned write.
  5. Keep only the live frame in previousFrame; track its physical start row after the recovery prefix so later partial redraws and appendToScrollback operate relative to the live frame.
  6. Treat recovery output as detached and one-shot. A subsequent append must produce recovered old output → new output → live frame in chronological order.
  7. Validate the row budget inside Siglyph and expose recovered-row count through diagnostics. Oversized provider output should fail or be deterministically bounded rather than silently corrupting cursor state.
  8. Preserve normal-screen operation and never clear scrollback or enter alternate-screen mode.

A text-only first version may be the safest contract. If typed controls are allowed, their encoding, image-ID remapping, cleanup, and completion ownership should follow the same closed boundary as append-only output; raw escape output should not be accepted.

Suggested acceptance coverage

  • Publish durable A, resize and recover A, then publish durable B; visible order is exactly A, B, retained live frame.
  • Durable rows already above the old viewport remain only in preserved scrollback and are not replayed.
  • Only the current-width tail that fits maxRows is recovered.
  • Ordinary status/editor/footer redraws never invoke recovery.
  • Repeated resizes preserve ordering without duplicating physical scrollback.
  • A live frame taller than the viewport yields a zero-row recovery budget safely.
  • Provider failure and oversized output preserve bounded diagnostics and fail without unsynchronized terminal writes.
  • Cursor position, append callbacks, FIFO append order, stop races, JVM behavior, and Scala Native behavior remain covered.

Alternatives considered

Replay the transcript as part of the retained frame

This reverses chronology for later append operations and can duplicate preserved scrollback.

Retain every appended component inside TUI

This conflicts with the existing detached, one-shot append lifecycle, especially for resource-owning typed image controls. Applications also have better semantic information for current-width reflow and deterministic eviction.

Disable viewport clearing on resize

A no-clear policy cannot generally guarantee removal of stale rows or correct cursor geometry across terminal emulators.

Let the application write recovery bytes directly

This breaks TUI terminal ownership and duplicates protocol, cursor, synchronization, and cleanup logic.

Compatibility notes

This can be additive and opt-in alongside NormalResizeClearPolicy.PreserveScrollback. Existing applications without a recovery provider should retain current behavior. The contract should be implemented consistently on JVM and Scala Native and preserve the closed typed-control boundary.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions