Problem or use case
A normal-screen interactive console can have two different kinds of output:
- durable, append-only history published with
TUI.appendToScrollback; and
- 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:
- Invoke the provider only for a geometry-changing normal-screen resize, not initial render, forced render, ordinary differential redraw, or append publication.
- Render the retained live frame first, then calculate a strict recovery budget such as
max(0, terminalHeight - visibleLiveFrameRows).
- 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.
- After the viewport clear, serialize recovery output followed by the retained live frame in one TUI-owned write.
- 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.
- Treat recovery output as detached and one-shot. A subsequent append must produce
recovered old output → new output → live frame in chronological order.
- 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.
- 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.
Problem or use case
A normal-screen interactive console can have two different kinds of output:
TUI.appendToScrollback; andWith
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:
appendToScrollbackoperation is inserted above the retained frame, so chronology becomesnew output → recovered old 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 ofpreviousFrame.One possible API shape is:
The exact naming and registration mechanism are open, but suggested semantics are:
max(0, terminalHeight - visibleLiveFrameRows).previousFrame; track its physical start row after the recovery prefix so later partial redraws andappendToScrollbackoperate relative to the live frame.recovered old output → new output → live framein chronological order.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
A, resize and recoverA, then publish durableB; visible order is exactlyA,B, retained live frame.maxRowsis recovered.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
TUIThis 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.