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.