Files
clojure-tui/README.md
Adam Jeniski dab0a27e4d add comprehensive documentation for external users
Includes getting started guide, hiccup views reference,
full API documentation, and annotated example walkthroughs
with ASCII output examples.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 11:37:16 -05:00

5.8 KiB

Clojure TUI

A terminal user interface framework for Clojure, inspired by Bubbletea (Go). Build interactive CLI applications using the Elm Architecture with Hiccup-style views.

╭──────────────────────────────────────╮
│                                      │
│     ╭────────────────────╮           │
│     │   Counter: 42      │           │
│     ╰────────────────────╯           │
│                                      │
│   j/k: change  r: reset  q: quit     │
│                                      │
╰──────────────────────────────────────╯

Features

  • Elm Architecture - Predictable state management with init, update, and view
  • Hiccup Views - Declarative UI with familiar Clojure syntax
  • Two Runtimes - Full async (tui.core) or simple sync (tui.simple for Babashka)
  • Rich Styling - Colors (16, 256, true color), bold, italic, underline, and more
  • Layout System - Rows, columns, and boxes with borders
  • Input Handling - Full keyboard support including arrows, function keys, and modifiers

Quick Start

Installation

Add to your deps.edn:

{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}

Hello World

(ns myapp.core
  (:require [tui.simple :as tui]))

(defn update-fn [model msg]
  (if (tui/key= msg "q")
    [model tui/quit]
    [model nil]))

(defn view [model]
  [:col
   [:text {:fg :cyan :bold true} "Hello, TUI!"]
   [:text {:fg :gray} "Press q to quit"]])

(tui/run {:init {}
          :update update-fn
          :view view})

Output:

Hello, TUI!
Press q to quit

Counter Example

(ns myapp.counter
  (:require [tui.simple :as tui]))

(defn update-fn [model msg]
  (cond
    (tui/key= msg "q")     [model tui/quit]
    (tui/key= msg "k")     [(inc model) nil]
    (tui/key= msg "j")     [(dec model) nil]
    (tui/key= msg "r")     [0 nil]
    :else                  [model nil]))

(defn view [model]
  [:col
   [:box {:border :rounded :padding [0 2]}
    [:text {:fg :yellow :bold true} (str "Count: " model)]]
   [:text {:fg :gray} "j/k: change  r: reset  q: quit"]])

(tui/run {:init 0
          :update update-fn
          :view view})

Output:

╭──────────────╮
│  Count: 0    │
╰──────────────╯
j/k: change  r: reset  q: quit

Running Examples

# With Babashka (simple sync runtime)
bb counter
bb timer
bb list
bb spinner
bb views
bb http

# With Clojure (full async support)
clojure -A:dev -M -m examples.counter

Documentation

Architecture

View (hiccup) → Render (ANSI string) → Terminal (raw mode I/O)
     ↑                                        │
     │                                        v
   Model  ←──────── Update ←─────────── Input (key parsing)

The application follows the Elm Architecture:

  1. Model - Your application state (any Clojure data structure)
  2. Update - A pure function (fn [model msg] [new-model cmd]) that handles messages
  3. View - A pure function (fn [model] hiccup) that renders the UI

Two Runtimes

tui.simple - Synchronous (Babashka-compatible)

Best for simple applications. No async support, but works with Babashka.

(require '[tui.simple :as tui])

(tui/run {:init model :update update-fn :view view-fn})

tui.core - Asynchronous (Full Clojure)

Full-featured runtime with async commands like timers and batched operations.

(require '[tui.core :as tui])

(defn update-fn [model msg]
  (case msg
    :tick [(update model :seconds inc) (tui/tick 1000)]
    [model nil]))

(tui/run {:init {:seconds 0}
          :update update-fn
          :view view-fn
          :init-cmd (tui/tick 1000)})

Hiccup View Elements

Element Description Example
:text Styled text [:text {:fg :red :bold true} "Error"]
:row Horizontal layout [:row "Left" "Right"]
:col Vertical layout [:col "Line 1" "Line 2"]
:box Bordered container [:box {:border :rounded} "Content"]
:space Empty space [:space {:width 5}]

Commands

Commands are returned from your update function to trigger side effects:

Command Description
tui/quit Exit the application
(tui/tick ms) Send :tick after ms milliseconds
(tui/batch c1 c2) Run commands in parallel
(tui/sequentially c1 c2) Run commands sequentially
(fn [] msg) Custom async function returning a message

Key Matching

(tui/key= msg "q")        ;; Character
(tui/key= msg :enter)     ;; Special key
(tui/key= msg :up)        ;; Arrow
(tui/key= msg [:ctrl \c]) ;; Control combo
(tui/key= msg [:alt \x])  ;; Alt combo

Project Structure

src/
  tui/
    core.clj      # Full async runtime (core.async)
    simple.clj    # Simple sync runtime (Babashka-compatible)
    render.clj    # Hiccup → ANSI
    terminal.clj  # Raw mode, input/output
    input.clj     # Key parsing
    ansi.clj      # ANSI codes, colors
examples/
    counter.clj
    timer.clj
    list_selection.clj
    spinner.clj
    views.clj
    http.clj

License

MIT