Append-only scrollback viewer for log lines, REPL output, chat transcripts, and similar streams.
The widget renders a fixed-size viewport into a longer line buffer. Apps own the buffer (typically a bounded Vector[String] or a ring-buffer) and a scrollOffset counted in display lines from the bottom — 0 is "tail of the buffer pinned to the last row" (the common live-feed view), positive values scroll back into history.
Long lines are wrapped to fit width when wrap = true (default), or truncated with a trailing … when wrap = false. Wrapping is computed by expand (which delegates per line to wrapLine); the visible window is sliced by viewport. Both helpers are public so apps can pre-compute display lines once per buffer change instead of every frame.
given Theme = Theme.dark
Layout.column(gap = 0)(
LogView(
lines = chatBuffer,
width = 60,
height = 12,
scrollOffset = 0 // pinned to live tail
): _*
)
Value parameters
at
Top-left cell of the viewport.
height
Cell height (number of rows rendered).
lines
The raw line buffer. The widget does not mutate it.
scrollOffset
Number of display lines scrolled up from the tail. 0 keeps the latest line on the last row. Negative values are treated as 0.
style
Optional style override for the rendered rows. Defaults to the theme's foreground slot.
width
Cell width of the viewport.
wrap
Whether to wrap long lines to fit width (default true) or truncate with ….
final case class Viewport(at: Coord, width: Int, height: Int)
Rectangle in absolute terminal cells the viewport occupies. Used by scrollDelta to decide whether a mouse-wheel event should drive the LogView; sized identically to the width / height / at arguments passed to LogView.apply.
Rectangle in absolute terminal cells the viewport occupies. Used by scrollDelta to decide whether a mouse-wheel event should drive the LogView; sized identically to the width / height / at arguments passed to LogView.apply.
Expand a raw line buffer into the display-line representation that the viewport will scroll over. Wrapped lines become multiple display lines; truncated lines become one. Empty input lines stay as empty display lines.
Expand a raw line buffer into the display-line representation that the viewport will scroll over. Wrapped lines become multiple display lines; truncated lines become one. Empty input lines stay as empty display lines.
Attributes
def maxScroll(lines: Seq[String], width: Int, height: Int, wrap: Boolean): Int
Maximum scroll offset for lines at this viewport size. Apps can use this to clamp scroll input from arrow keys or wheel events.
Maximum scroll offset for lines at this viewport size. Apps can use this to clamp scroll input from arrow keys or wheel events.
Map a MouseEvent.Scroll to a LogView scroll delta (negative for up / older lines, positive for down / newer lines), provided the scroll happened inside the viewport rectangle. Horizontal scroll (Left / Right) is ignored.
Map a MouseEvent.Scroll to a LogView scroll delta (negative for up / older lines, positive for down / newer lines), provided the scroll happened inside the viewport rectangle. Horizontal scroll (Left / Right) is ignored.
Apps wire this into their update:
case Msg.Key(InputKey.Mouse(ev)) =>
val viewportRect = LogView.Viewport(at, width, height)
LogView.scrollDelta(ev, viewportRect) match
case Some(d) => Tui(scrollBy(model, d))
case None => model.tui
Value parameters
event
The decoded mouse event.
ticksPerDetent
Lines moved per wheel detent. Defaults to 3, which roughly matches the OS-level scroll-acceleration users expect from a real terminal log pane. Pass 1 for one-line-per- detent semantics.
viewport
Viewport rectangle the scroll must land in.
Attributes
Returns
Some(delta) if the event is a vertical scroll inside the viewport, None otherwise.
Slice displayLines into the visible window: height lines anchored at scrollOffset from the tail. The result is padded on top with blank rows when the buffer is shorter than the viewport, so apps can draw a fixed-height region without conditional logic.
Slice displayLines into the visible window: height lines anchored at scrollOffset from the tail. The result is padded on top with blank rows when the buffer is shorter than the viewport, so apps can draw a fixed-height region without conditional logic.