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.

GleamErlang term (beamtea)
Quitquit
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:

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:

CommandEffect
Nonedo nothing
Quitquit 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

ModuleWhat it gives you
gleamteaprogram, start, start_with, Event, Cmd, Options, Layout, window_size
gleamtea/keythe Key type (Up, Enter, Ctrl(_), Char(_), …)
gleamtea/termANSI escapes: bold, faint, reverse, move, clear, fg_rgb, rule, …
gleamtea/colorthe Charm-inspired palette (Charmple, Hotpink, Cyan, …)
gleamtea/stylelipgloss-style blocks: colour, border, padding, align, join_*
gleamtea/layoutposition content: center, frame, place, overlay_center
gleamtea/utilANSI-aware lines, pad_visible, truncate, visible_width

Bubbles (reusable components)

ModuleComponent
gleamtea/spinneranimated spinner (8 styles)
gleamtea/progresstruecolor gradient progress bar
gleamtea/timercountdown timer
gleamtea/listscrollable, selectable list
gleamtea/tablescrollable, row-selectable data table
gleamtea/textinputsingle-line text input
gleamtea/textareamulti-line text editor
gleamtea/viewportscrollable viewport over a block of text
gleamtea/filepickerfilesystem picker
gleamtea/paginatorpaginator (dots or “N/M”)
gleamtea/keybinddeclarative key bindings
gleamtea/helphelp 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,
))

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>
ExampleShows off
counterthe core loop — no bubbles
keyshow raw input decodes into key events
stopwatchEvery timers and a User message
palettethe full colour palette
spinner_demothe spinner bubble, cycling styles
progress_demoa self-filling truecolor gradient bar + Every
timer_demoa 10-second countdown
list_demoa scrollable, selectable menu
table_demoa selectable data table
textinput_demoa focused text field with a live greeting
textarea_demoa multi-line editor in a bordered box
viewport_demoscrolling through a long document
filepicker_demobrowsing the filesystem
help_demokey bindings + help + paginator
dashboard_demoa grid of styled stat cards (style composition)
statusbar_demoa full-width vim-style status bar
tabs_demoa tabbed interface with a bordered panel
modal_demoa floating yes/no modal over a backdrop
finder_demoan 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

Search Document