tea_term

The terminal half of the Elm architecture: the widget vocabulary, its DSL, its renderer, the row presenter, routing, and the driver loop.

use tea_core/app.{App}
use tea_core/cmd.{Cmd}
use tea_term/step.{step}
use tea_term/widget.{Widget, Text, Row, Button}

impl App[Counter] {
  type Msg = CounterMsg
  type View = Widget[CounterMsg]
  fn update(m: Counter, msg: CounterMsg) -> (Counter, Cmd[CounterMsg]) = ...
  fn view(m: Counter) -> Widget[CounterMsg] = ...
}

The Tree contract, the reconciler, trait App and subscriptions are packages/tea-core's. examples/projects/tea_todo is a complete app: todo.dawn is the pure half with its tests, main.dawn the io.

Widgets

Widget[M] is Text, Styled, Row, Column and Button(label, on_press: M). A button holds the message a press means, never a callback, so a tree has structural == and a view test is one assertion: view(m) == expected.

update, view, render, step, present and the routing functions are pure. The one !io is runtime.run, the driver loop, which an app hands its pure hooks: the view, a parser from an input line to a message, its ticks and when to stop. A SendMsg command is one more update before the next paint.

Subscriptions

run reads the monotonic clock (std/io's Clock), so Tick(every_ms, msg) fires every every_ms of elapsed time, typing, updating and painting included. A tick that a slow turn made late fires once; periods missed whole are skipped, not delivered in a burst. Before 0.4.0 the loop had no clock and counted only full poll timeouts, so steady input could hold a timer off indefinitely.

At end of input the session ends, timers or not: the native backend's poll answers at once for a closed stream, the loop sees from the clock that the answer came early, and reads. The JVM backend cannot tell a closed pipe from a silent one, so there a subscribed app on piped input keeps ticking until done. run installs the real clock itself, so its signature has no !Clock. The timing rules are in runtime.dawn's header.

The DSL

tea_term/dsl has lowercase wrappers over the constructors: text("hi"), bold / dim / underline, button(label, msg), row([...]) and column([...]). row_do { ... } / column_do { ... } take a block that may hold lets before the child list.

Type arguments flow from the expected type, and a let has none: text("a") infers M as a list element, a return value or an annotated binding, but not as a bare segment of a ++ chain. Write a view as annotated lets (let header: Widget[Msg] = ...) and join the named parts. And the tail block is a thunk: row_do is for when a statement belongs next to the children, and never shorter than row([...]).

Rendering and presenting

render lays out a tree: Row joins children with a space, Column with a newline, Button shows [label], Styled wraps its child in ANSI SGR codes. clear_screen() is the redraw sequence.

present(prev, next) compares two rendered frames row by row and answers the cursor-addressed bytes that rewrite only the rows that changed, or "" for an identical frame. When the new frame is shorter it also erases the row below the old frame, where the previous prompt was parked. frame_lines cuts a frame into rows and park(nrows) puts the prompt below it. The caller keeps the rows it last painted.

Routing

route.buttons(w) lists every button in pre-order, route.press(w, n) is the message of the n-th one (1-based) or None, and route.addressed(w) keys the same walk by patch address.

Changes between versions: CHANGELOG.md.

API

tea_term/dsl

dsl.text

fn text[M](s: String) -> Widget[M]

Text(s: ...), unceremoniously.

dsl.bold

fn bold[M](child: Widget[M]) -> Widget[M]

Bold wrapper: bold(text("hi")).

dsl.dim

fn dim[M](child: Widget[M]) -> Widget[M]

Dim wrapper.

dsl.underline

fn underline[M](child: Widget[M]) -> Widget[M]

Underline wrapper.

dsl.button

fn button[M](label: String, on_press: M) -> Widget[M]

Button(label:, on_press:), positionally: what it shows, then what pressing it means.

dsl.row

fn row[M](kids: List[Widget[M]]) -> Widget[M]

A row of children, as a plain list argument.

dsl.column

fn column[M](kids: List[Widget[M]]) -> Widget[M]

A column of children, as a plain list argument.

dsl.row_do

fn row_do[M](body: fn() -> List[Widget[M]]) -> Widget[M]

row, tail-block spelling: the block may hold statements and ends with the child list. Same tree, one thunk dearer; prefer row(...) for a single expression.

dsl.column_do

fn column_do[M](body: fn() -> List[Widget[M]]) -> Widget[M]

column, tail-block spelling; see row_do.

tea_term/present

present.frame_lines

fn frame_lines(frame: String) -> List[String]

A rendered frame cut into rows, the unit present diffs at.

present.present

fn present(prev: List[String], next: List[String]) -> String

The bytes that turn a screen showing prev into one showing next. Identical frames emit nothing at all.

present.park

fn park(nrows: Int) -> String

Park the cursor on the first row below an nrows-tall frame and clear that row; a driver prints its prompt there, over the previous prompt.

tea_term/render

render.render

fn render[M](w: Widget[M]) -> String

The whole screen a widget tree describes, ANSI styling included.

render.clear_screen

fn clear_screen() -> String

The escape sequence that clears the terminal and homes the cursor; a redraw-style driver prints this before each frame. Pure on purpose: the caller decides where the bytes go.

tea_term/route

route.buttons

fn buttons[M](w: Widget[M]) -> List[(String, M)]

Every button in the tree, in pre-order: label and the message pressing it sends. The n-th entry is what press(w, n) resolves (1-based).

The walk is tea_core/walk.fold_preorder and the reading of a node is here, which is the whole reason routing did not become a second core contract: core has no way to ask a node for its label, so a router written against a core trait would have had to give the label up.

route.addressed

fn addressed[M](w: Widget[M]) -> List[(List[Int], M)]

The same walk, addressed rather than numbered: every button paired with the path tea_core/diff would use for it. A line-mode driver has no use for this -- it numbers -- but a host that hands back a node rather than an ordinal dispatches on exactly these addresses, with no second scheme.

route.press

fn press[M](w: Widget[M], n: Int) -> Option[M]

The message the n-th button would send, counting from 1 in pre-order, or None when there is no such button.

tea_term/runtime

runtime.run

fn run[A: App](init: A, view_of: fn(A) -> Widget[A.Msg], parse: fn(A, String) -> A.Msg, subs: fn(A) -> List[Sub[A.Msg]], done: fn(A) -> Bool) -> Unit !(A.E|io)

Drive init until done: clear once, paint, then loop. Timers re-arm only when the declared intervals change, so an unrelated model edit does not reset a running timer; messages are looked up in the declaration current at fire time, never from the armed state.

tea_term/step

step.step

fn step[A: App](m: A, msg: A.Msg, vw: fn(A) -> Widget[A.Msg]) -> (A, String) !A.E

Apply the message and everything its command asked for, then render the new view. A driver prints the frame and reads the next message; a test asserts on it.

The fold is tea_core/cmd's, so this driver and the DOM one discharge a command the same way and neither owns a copy of the rule.

step.diff_step

fn diff_step[A: App, W: Tree + Eq](m: A, msg: A.Msg, vw: fn(A) -> W) -> (A, List[Patch[W]]) !A.E

The same turn for a host that wants the change rather than the frame: the new model and the patches from the old view to the new one. W is the driver's own type parameter, which is what lets Tree + Eq apply to it at all; the caller's view settles it to a concrete widget tree.

tea_term/widget

impls registered by this module (visible program-wide, no use needed): Show[Style], Show[Widget[M]], Tree[Widget[M]]

Style

type Style = Plain | Bold | Dim | Underline

How the terminal renderer may dress a subtree. Plain is the identity and exists so that "which style" can be computed: an if over styles needs a spelling for "none".

Widget

type Widget[M] = Text(s: String) | Styled(style: Style, child: Widget[M]) | Row(kids: List[Widget[M]]) | Column(kids: List[Widget[M]]) | Button(label: String, on_press: M)

A pure description of a screen. M is the application's message type: the tree says what an interaction means (Button(label, on_press)), and holds no code. Row lays children out on one line, Column stacks them.