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.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.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
本模块登记的 impl(全程序可见,不必 use):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.