Files
clojure-tui/README.md
Adam Jeniski 507db9cf00 add VHS-generated GIFs for all examples
Created tape files and GIFs demonstrating each example:
- counter, timer, list, spinner, views, http
- Updated README with GIF showcase

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 12:44:38 -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 Example

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

Examples

Counter

Simple counter demonstrating basic Elm Architecture.

Counter

bb counter

Timer

Countdown timer with pause/resume - demonstrates async commands.

Timer

bb timer

List Selection

Multi-select list with cursor navigation.

List Selection

bb list

Spinner

Animated loading spinners with multiple styles.

Spinner

bb spinner

Views

State machine pattern with multiple views and navigation.

Views

bb views

HTTP

Async HTTP requests with loading states.

HTTP

bb http

Running with Full Clojure

For full async support (core.async), run with Clojure:

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