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.

maxFooterRows?: number

Clamp footer growth to this many rows, always at most one less than the terminal height. Defaults to 20. A taller footer keeps its last rows.

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.

footerRows(): number

Rows the footer currently occupies.

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();