Maintains a Vector[String] of lines plus a (row, col) cursor. Editing keys (CharKey, Enter, Backspace, Delete, arrow keys, Home/End) compose to a small fixed-pitch text editor — enough for a comments box, a description field, or the body of a chat message. For single-line input use termflow.tui.Prompt instead.
Cursor navigation is grapheme-aware within a line: arrow keys step over surrogate pairs and combining marks as one unit, mirroring the single-line Prompt. The cursor is stored in UTF-16 code-unit offsets to match the underlying String model — apps composing with other text APIs can pass it straight through.
The widget renders one TextNode per visible row. The cursor row uses a reverse-video cell so the caret is visible without having to commandeer the hardware cursor — same approach as termflow.tui.widgets.TextField. Lines too wide for the viewport are clipped with a trailing …; vertical scrolling lifts the top row off-screen when the cursor moves below the visible window.
The widget is purely presentational. State lives in the calling app:
given Theme = Theme.dark
MultiLineInput.render(
state = m.editor,
width = 60,
height = 8,
at = Coord(2.x, 5.y)
)
Apps wire the keyboard with MultiLineInput.handleKey(state, key) which returns (newState, Option[Cmd[Msg]]) — the optional command is only ever None today (no submit semantics are baked in), but the shape matches Prompt.handleKey so swapping the two is a small change.
final case class State(lines: Vector[String], cursorRow: Int, cursorCol: Int, scrollTop: Int)
Editor state.
Editor state.
Invariant: lines always contains at least one entry — an empty editor is lines = Vector("") with cursor (0, 0). The cursor row is clamped to [0, lines.size - 1] and the column to [0, currentLine.length] after every transition.
Value parameters
cursorCol
Zero-based UTF-16 column on the cursor's row.
cursorRow
Zero-based row of the cursor.
lines
The editor's lines, top-to-bottom. UTF-16 code-unit strings, no trailing newline.
scrollTop
First visible row (vertical scroll offset). Internal-state-ish; apps generally don't manage it directly — handleKey keeps it consistent with the cursor.
Feed a key event into the editor and return the updated state.
Feed a key event into the editor and return the updated state.
Returns (state, None) for handled keys; (state, None) for unhandled keys (no-op) — the Option[Cmd] shape is reserved for future submit semantics so callers don't have to refactor when those land.
Feed a key event into the editor with submit semantics.
Feed a key event into the editor with submit semantics.
When key matches submitKey, the full text (state.text) is passed to toMsg and the resulting Cmd is returned. The editor resets to State.empty. For all other keys, editing proceeds identically to the base handleKey.
Value parameters
key
The key event to process.
state
Current editor state.
submitKey
The key that triggers text submission.
toMsg
Callback to convert the submitted text into an app message. Return Left(error) to surface a termflow.tui.TermFlowError without submitting.
Render the editor at (at, width, height). Returns a list of VNodes — one per visible row plus an InputNode for the cursor row. Apps typically embed this in a BoxNode border for a nicer visual; the widget doesn't draw the border itself.
Render the editor at (at, width, height). Returns a list of VNodes — one per visible row plus an InputNode for the cursor row. Apps typically embed this in a BoxNode border for a nicer visual; the widget doesn't draw the border itself.
Value parameters
at
Top-left of the viewport.
height
Visible height in rows.
state
Current editor state.
width
Visible width in cells (rows are clipped past this).