inline
js/tty/inline.ts
fino:tty/inline — inline terminal rendering with a pinned footer.
Where render() from fino:tty/tui takes the alternate screen and owns the
viewport, this renderer stays in the primary buffer. Finalized output is
pushed into the terminal's own scrollback — natively selectable, scrollable,
and still there after the process exits — while a small footer pinned to the
bottom rows is repainted in place. That is the shape a REPL or a coding agent
wants: a transcript the terminal owns, above a composer the app owns.
The footer is a component tree, laid out through the same retained pipeline
render() uses, so focus, key dispatch and the component catalog all work
inside it. Only viewport ownership differs.
Pushing history uses a DECSTBM scroll region confined to the rows above the
footer: the cursor parks on the last occupied history row and writes \r\n
per line, which fills any blank rows first and then scrolls the region, so
rows evicted off the top enter real scrollback while the footer stays put.
The region is always reset inside the same composed write, so an interrupted
process never leaves the terminal with a stale margin.
Mouse capture is off by default. Inline mode exists so the terminal keeps selection, scrolling and find; an app that needs pointer input can turn it on per app or take the alternate screen for a modal overlay.
import { Text } from 'fino:ui/components';
import { renderInline } from 'fino:tty/inline';
const app = renderInline(() => Text({ children: ['> '] }), {
onEvent(event) {
if (event.type === 'key' && event.key === 'enter') app.printAbove(['committed']);
},
});Interfaces
interface InlineOptions {
Options for {@link renderInline}.
Properties
width?: number
Fixed width in cells; defaults to the terminal width and tracks resizes.
input?: boolean
Read keyboard input; defaults to true, and an onEvent handler implies it.
mouse?: boolean
Capture the mouse. Defaults to false — inline mode exists to leave selection, scrolling and find to the terminal.
onEvent?: (event: TuiEvent, app: InlineApp) => void | Promise<void>
Called for each decoded event no tree handler consumed.
onResize?: (size: TerminalSize, app: InlineApp) => void
Called after geometry is re-established at a new terminal size.
interface InlineApp {
Handle returned by {@link renderInline}.
Methods
printAbove(content: VNode | string[]): void
Commit finalized content into the terminal's scrollback, above the footer.
A component tree is laid out at the current width and committed row by row. Strings are written verbatim, so pre-wrap them: a line wider than the terminal soft-wraps and evicts an extra row.
resetHistory(): void
Clear the screen and the terminal's scrollback and start committing from the top again.
Rows already committed cannot be re-wrapped, so an app whose transcript should follow a new terminal width has to discard and re-emit it. Anything the reader had scrolled back to is lost — this is for deliberate rebuilds, not routine repainting.
update(element: VNode): void
Replace the footer tree, stopping any reactive root given to renderInline.
size(): TerminalSize
Current terminal size.
setMouse(enabled: boolean): boolean
Toggle mouse capture, returning the resulting state.
frame(): Frame | null
The most recently laid-out footer frame.
stop(): Promise<void>
Clear the footer, park the cursor on the row after the last committed line, and restore terminal state. Idempotent.
Resolves once the restore has reached the terminal, so a caller that exits
the process immediately afterwards cannot truncate it. Letting the runtime
drain normally works too; awaiting only matters before exit().
Properties
focus: TuiFocus
Focus traversal over the footer's retained tree.
input?: TuiInput
Raw input reader when input was enabled.
Functions
function renderInline(
element: VNode | (() => VNode),
options: InlineOptions = {},
): InlineApp
Render a component tree as an inline footer above the terminal's scrollback.
Passing a function makes the footer reactive: every signal read while rendering becomes a dependency and the footer repaints when one changes.
import { Text } from 'fino:ui/components';
import { renderInline } from 'fino:tty/inline';
const app = renderInline(Text({ children: ['ready'] }));
app.printAbove(['a finished line']);
app.stop();