Typed Gleam bindings for beamtea — Bubble Tea / The Elm Architecture for the BEAM terminal.

Files
README.md

gleamtea #

Package Version Hex Docs

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.

gleamtea preview

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 (like vim/less). Default True.
  • catch_ctrl_c — quit on Ctrl-C instead of passing it to update. Default True.
  • 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.

License #

MIT