# gleamtea

[![Package Version](https://img.shields.io/hexpm/v/gleamtea)](https://hex.pm/packages/gleamtea)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleamtea/)

**A delightful terminal UI framework for Gleam** — typed bindings to
[beamtea](https://hex.pm/packages/beamtea), which is
[Bubble Tea](https://github.com/charmbracelet/bubbletea) / 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](.github/assets/preview.png)

```gleam
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 `String`s.

---

## Installation

```sh
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)`:

```gleam
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                             |

```gleam
// 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:

```gleam
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):

```gleam
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`:

```gleam
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`:

```gleam
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:

```sh
./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

```gleam
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/`](src/examples). Run any of
them with the launcher:

```sh
./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

```sh
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](https://github.com/tsirysndr/beamtea)
by Tsiry Sandratraina, which is itself inspired by Charmbracelet's
[Bubble Tea](https://github.com/charmbracelet/bubbletea) and
[Bubbles](https://github.com/charmbracelet/bubbles).

## License

[MIT](LICENSE)
