Inline mode
By default a sumi app takes over the whole terminal using the alternate screen, like a full-screen TUI. Inline mode renders at the shell cursor instead, drawing a live region in the normal buffer that grows and shrinks in place and leaves its final frame behind in scrollback when the app exits. This is the model behind installers, progress displays, prompts, and REPL-style tools that should read as ordinary command output rather than take over the screen.
Enabling it
Inline mode is a field on RunOptions:
tui.RunWithOptions(comp, tui.RunOptions{
Inline: true,
}) tui.Run(comp) is the full-screen shortcut and leaves Inline false. The
other RunOptions fields (In, Out, ExitOn, ColorScheme, Mouse, and
so on) work the same in both modes.
The live-zone model
Inline mode maintains a live zone: a block of rows at the shell cursor that
sumi owns and repaints. Crucially, it never learns the zone’s absolute position
on screen — it moves the cursor relatively. Rows move with cursor-up and
cursor-down; columns move with an absolute column command (CHA), which is
safe because column 1 is always column 1. It never emits an absolute cursor
position, and it never enters the alternate screen.
Growth and shrink are handled without scrolling surprises:
- Growing the zone appends real newlines (line feeds), because a line feed can scroll the viewport when the cursor is at the bottom whereas a cursor-down cannot.
- Shrinking erases from the cursor to the end of the display and leaves the now-blank physical rows realised, so re-growing back into them repaints without emitting fresh newlines.
Each render diffs the new frame against the previous one and rewrites only the cells that changed, moving relatively between them. A width change erases the zone in place and repaints it fully.
Exit and scrollback
On exit sumi parks the cursor on a fresh line just below the rendered content and shows it again. The final frame is not erased — it stays in the terminal’s scrollback as normal output, so the last state of the app remains visible in the session history, exactly like the output of any other command.
Suspend
When the user suspends the app with Ctrl+Z, inline mode finishes the current frame (parking the cursor below it) and then forgets the zone entirely. While the process is stopped the shell owns the screen, so on resume (SIGCONT) sumi starts a fresh zone wherever the cursor now is and re-discovers its origin (see mouse, below) rather than assuming the old position is still valid.
The frame log
For append-only output — a stream of results, log lines, or completed
steps — inline mode offers a FrameLog. Each frame is a full sumi component
mounted into the live zone; frames stack in block flow under a host container
you place in your tree. The point of a frame log is that finished frames can be
handed to the terminal’s native scrollback with no repaint.
log := tui.NewFrameLog()
log.ReleaseTop = app.ReleaseTop // wire archiving to the inline app
id := log.Append(resultComponent) // mount a new frame, returns its id
// update a live frame by writing its signals; the next render reflects it
log.Archive(id) // hand its rows (and any above it) to scrollback The three operations:
Append(c *Component) intmounts a component as a new frame at the bottom of the log and returns its id.Archive(id int)releases the rows of that frame and every frame above it into native scrollback, then disposes those components. Archiving is cumulative from the top, which matches how output scrolls off: everything older than the archived frame goes with it.Remove(id int)disposes a single frame without archiving it — its rows are cleared and the zone reflows. Use this to redact a frame rather than commit it to history.
Wiring ReleaseTop to the app’s ReleaseTop is what connects archiving to real
scrollback; the released rows already sit on the terminal, so handing them over
emits nothing and simply narrows the region sumi keeps diffing. Left nil (or in
full-screen mode) Archive becomes dispose-only.
Mouse
Mouse events arrive from the terminal in absolute screen coordinates, but the
live zone only knows its own rows. To translate, inline mode discovers its
origin with a cursor-position report: it snapshots the cursor’s zone row, sends
a Device Status Report (ESC [ 6n), and when the terminal replies with the
current cursor position it computes the screen row of zone row 0. It re-queries
after resize and after suspend, since either can move the zone.
With the origin known, incoming mouse rows are mapped back into zone coordinates; a click outside the zone (or before the origin is known) is dropped rather than misattributed. If the zone has grown and scrolled up so its bottom is pinned to the screen bottom, the origin is clamped accordingly so the mapping stays correct.