- Gleam 85%
- Erlang 13%
- Shell 2%
gleamtea #
A delightful terminal UI framework for Gleam — typed bindings to
beamtea, which is
Bubble Tea / The Elm Architecture
for the BEAM, built on prim_tty.
Write your terminal app as three pure functions — init, update, view —
and gleamtea owns the terminal, decodes keypresses, runs your commands, and
repaints the screen.

import gleamtea.{type Cmd, type Event, Key, None, Quit}
import gleamtea/color
import gleamtea/key.{Char, Down, Up}
import gleamtea/term
import gleam/int
pub fn main() {
gleamtea.program(init:, update:, view:)
|> gleamtea.start
}
fn init() -> #(Int, Cmd(Event(Nil))) {
#(0, None)
}
fn update(msg: Event(Nil), n: Int) -> #(Int, Cmd(Event(Nil))) {
case msg {
Key(Up) -> #(n + 1, None)
Key(Down) -> #(n - 1, None)
Key(Char(113)) -> #(n, Quit)
// q
_ -> #(n, None)
}
}
fn view(n: Int) -> String {
color.fg(color.Charmple, term.bold("count: " <> int.to_string(n)))
}
Why this is (almost) all types and no glue #
beamtea speaks plain Erlang terms: the atom quit, the tuple {tick, 1000, Msg},
a keypress {key, up}. Gleam's custom types compile to exactly those terms —
so the binding is mostly a set of type definitions with zero marshalling cost.
| Gleam | Erlang term (beamtea) |
|---|---|
Quit |
quit |
Tick(1000, msg) |
{tick, 1000, Msg} |
Key(Up) |
{key, up} |
Key(Char(97)) |
{key, {char, 97}} |
Key(Ctrl(99)) |
{key, {ctrl, 99}} |
The escapes and colours (gleamtea/term, gleamtea/color) and the reusable
components (gleamtea/spinner, gleamtea/table, …) are thin FFI wrappers that
flatten beamtea's iodata() into ordinary Gleam Strings.
Installation #
gleam add gleamtea
beamtea (an Erlang/rebar3 package) is pulled in automatically as a
dependency; Gleam builds it with rebar3.
The Elm Architecture #
A program is three functions, wired together with gleamtea.program:
init() -> #(model, Cmd(msg))— the starting model and an initial command.update(msg, model) -> #(model, Cmd(msg))— fold a message into the model.view(model) -> String— render the model to the full screen.
Your message type is always Event(user):
pub type Event(user) {
Key(Key)
// a keypress — see gleamtea/key
Resize(#(Int, Int))
// the terminal was resized to #(cols, rows)
User(user)
// one of your own messages
}
Keyboard and resize events arrive as Key(...) / Resize(...). Your own
messages — produced by commands like Tick, Every and Task — arrive
wrapped in User(...), so everything stays type-safe.
Commands #
Return a Cmd(msg) from init/update to ask the runtime to do something:
| Command | Effect |
|---|---|
None |
do nothing |
Quit |
quit after this update |
Msg(m) |
deliver m to update as soon as possible |
Tick(ms, m) |
deliver m once, after ms milliseconds |
Every(ms, m) |
deliver m repeatedly, every ms milliseconds |
Task(fn) |
run fn asynchronously; its result is delivered to update |
Batch([...]) |
run several commands |
Seq([...]) |
run several commands in sequence |
// tick every 100 ms
#(model, Every(100, User(Tick)))
Modules #
Core #
| Module | What it gives you |
|---|---|
gleamtea |
program, start, start_with, Event, Cmd, Options, Layout, window_size |
gleamtea/key |
the Key type (Up, Enter, Ctrl(_), Char(_), …) |
gleamtea/term |
ANSI escapes: bold, faint, reverse, move, clear, fg_rgb, rule, … |
gleamtea/color |
the Charm-inspired palette (Charmple, Hotpink, Cyan, …) |
gleamtea/style |
lipgloss-style blocks: colour, border, padding, align, join_* |
gleamtea/layout |
position content: center, frame, place, overlay_center |
gleamtea/util |
ANSI-aware lines, pad_visible, truncate, visible_width |
Bubbles (reusable components) #
| Module | Component |
|---|---|
gleamtea/spinner |
animated spinner (8 styles) |
gleamtea/progress |
truecolor gradient progress bar |
gleamtea/timer |
countdown timer |
gleamtea/list |
scrollable, selectable list |
gleamtea/table |
scrollable, row-selectable data table |
gleamtea/textinput |
single-line text input |
gleamtea/textarea |
multi-line text editor |
gleamtea/viewport |
scrollable viewport over a block of text |
gleamtea/filepicker |
filesystem picker |
gleamtea/paginator |
paginator (dots or "N/M") |
gleamtea/keybind |
declarative key bindings |
gleamtea/help |
help view that renders keybind bindings |
A bubble is a little model with its own update/view. Store it in your model,
render it in your view, and forward messages you don't handle to its
update — self-animating bubbles (spinner, timer) keep themselves running by
returning their next tick:
fn update(msg: Msg, model: Model) -> #(Model, Cmd(Msg)) {
case msg {
Key(Char(113)) -> #(model, Quit)
// q
other -> {
let #(spinner, cmd) = spinner.update(other, model.spinner)
#(Model(..model, spinner:), cmd)
}
}
}
Styling & layout (beamtea 0.1.2) #
gleamtea/style is a composable, lipgloss-inspired styling layer. Build a
Style with piped setters, then render it onto text — inline attributes
(colour, bold) compose with block layout (width, alignment, padding, border):
import gleamtea/style
import gleamtea/color
style.new()
|> style.foreground(style.Named(color.Charmple))
|> style.bold
|> style.padding(style.VH(1, 3))
|> style.border(style.Rounded)
|> style.border_foreground(style.Rgb(0xEE, 0x6F, 0xF8)) // 24-bit truecolor
|> style.render("Hello, style!")
Colours can be a palette Named(_), a raw xterm Index(_), or 24-bit
Rgb(r, g, b). Compose rendered blocks with style.join_horizontal and
style.join_vertical.
gleamtea/layout positions a block within the terminal — pair it with
gleamtea.window_size() to size to the screen from inside your view:
import gleamtea/layout
let #(cols, rows) = gleamtea.window_size()
layout.center(card, cols, rows) // centre a block
layout.frame(content, cols, rows, color.Indigo) // bordered full-screen panel
layout.overlay_center(background, modal, cols, rows) // float a modal
Or let the runtime position the whole view automatically (it re-flows on
resize) with the layout option — TopLeft (default), Center, TopCenter
or Fill:
gleamtea.start_with(program, gleamtea.Options(
alt_screen: True,
catch_ctrl_c: True,
layout: gleamtea.Center,
))
Running a program #
A full-screen TUI needs to own the terminal: raw mode, the alternate screen,
and Ctrl-C delivered as a byte rather than a signal. Don't launch from an
interactive gleam run (its shell fights you for the terminal). Instead use the
included launcher, which starts the VM with -noshell -noinput +Bi and puts the
tty in raw mode:
./bin/gleamtea-run counter
Under the hood it runs the compiled example's main/0; do the same for your own
app once it's compiled.
Options #
gleamtea.program(init:, update:, view:)
|> gleamtea.start_with(gleamtea.Options(
alt_screen: True,
catch_ctrl_c: True,
layout: gleamtea.TopLeft,
))
alt_screen— use the alternate screen buffer (likevim/less). DefaultTrue.catch_ctrl_c— quit on Ctrl-C instead of passing it toupdate. DefaultTrue.layout— position the whole view:TopLeft(default),Center,TopCenter,Fill.
start / start_with return Ok(final_model) on a clean exit, or
Error(reason) if the program could not start (e.g. stdout is not a terminal).
Examples #
Nineteen runnable examples live in src/examples/. Run any of
them with the launcher:
./bin/gleamtea-run <name>
| Example | Shows off |
|---|---|
counter |
the core loop — no bubbles |
keys |
how raw input decodes into key events |
stopwatch |
Every timers and a User message |
palette |
the full colour palette |
spinner_demo |
the spinner bubble, cycling styles |
progress_demo |
a self-filling truecolor gradient bar + Every |
timer_demo |
a 10-second countdown |
list_demo |
a scrollable, selectable menu |
table_demo |
a selectable data table |
textinput_demo |
a focused text field with a live greeting |
textarea_demo |
a multi-line editor in a bordered box |
viewport_demo |
scrolling through a long document |
filepicker_demo |
browsing the filesystem |
help_demo |
key bindings + help + paginator |
dashboard_demo |
a grid of styled stat cards (style composition) |
statusbar_demo |
a full-width vim-style status bar |
tabs_demo |
a tabbed interface with a bordered panel |
modal_demo |
a floating yes/no modal over a backdrop |
finder_demo |
an fzf-style fuzzy finder in a floating modal |
Development #
gleam build # compile the library, bindings and examples
gleam test # run the test suite
gleam docs build # generate HTML docs into build/dev/docs
./bin/gleamtea-run counter
Credits #
gleamtea is a thin Gleam layer over beamtea by Tsiry Sandratraina, which is itself inspired by Charmbracelet's Bubble Tea and Bubbles.