/eɪˈtwiː/ · n. · a case for holding small precious things
Build beautiful terminal interfaces in pure Gleam.
Buffer-diff rendering, composable widgets, Unicode-correct grapheme clusters, crash-safe terminal restore. On the BEAM.
Spacing and alignment in this web preview depend on the browser font renderer and the monospace fallback in use. In a real terminal with a proper monospace font, box-drawing characters and cell widths align pixel-perfect.
// any fn(Buffer, Rect) → Buffer is a widget — no traits, no registration
import gatui/widget
// compose: border wrapping inner content
let w = widget.compose(border_w, block.inner(area, blk), content_w)
// layer: draw top over bottom
let w = widget.layer(background_w, overlay_w)
// animated widget: receives frame counter
let aw: widget.AnimatedWidget = fn(buf, area, frame) { ... }
widget.freeze_frame(aw, current_frame)(buf, area) import gatui/theme
let t = theme.catppuccin_mocha()
let t = theme.nord()
let t = theme.tokyo_night()
// customize from a base
let custom = theme.Theme(..theme.nord(), accent: style.Rgb(255, 165, 0)) [dependencies]
etui = ">= 0.1.0 and < 1.0.0" import gatui/app
import gatui/backend/erlang
import gatui/buffer
import gatui/geometry.{Fill, Horizontal, Percentage}
import gatui/widgets/block
import gatui/widgets/paragraph
pub type Model { Model(count: Int, width: Int, height: Int) }
pub fn main() {
app.run(erlang.new(), Model(0, 80, 24), view, update, fn(m) { m.count >= 10 }, 16)
}
fn view(model: Model) -> List(app.RenderOp) {
let area = geometry.rect_new(0, 0, model.width, model.height)
let chunks = geometry.split(Horizontal, area, [Percentage(30), Fill])
let buf =
buffer.buffer_new(area)
|> block.render(left, block.block_new() |> block.with_border(block.Single))
|> paragraph.render(right, paragraph.paragraph_new("Count: " <> int.to_string(model.count)))
[app.Render(buf)]
}