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>
9.3 KiB
Getting Started
This guide will walk you through creating your first TUI application with Clojure TUI.
Installation
Using deps.edn
Add the library to your deps.edn:
{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}
Requirements
- Clojure 1.11+ for full async runtime (
tui.core) - Babashka for simple sync runtime (
tui.simple) - A terminal that supports ANSI escape codes (most modern terminals)
Your First Application
Let's build a simple "Hello World" TUI application.
Step 1: Create the Project
mkdir my-tui-app
cd my-tui-app
Create a deps.edn:
{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}
Step 2: Write the Application
Create src/myapp/core.clj:
(ns myapp.core
(:require [tui.simple :as tui]))
;; 1. Model - the application state
(def initial-model
{:message "Hello, TUI!"})
;; 2. Update - handle messages and return [new-model command]
(defn update-fn [model msg]
(cond
;; Quit on 'q' key
(tui/key= msg "q")
[model tui/quit]
;; Default: no change
:else
[model nil]))
;; 3. View - render the model as hiccup
(defn view [{:keys [message]}]
[:col
[:text {:fg :cyan :bold true} message]
[:space {:height 1}]
[:text {:fg :gray} "Press q to quit"]])
;; Run the application
(defn -main [& args]
(tui/run {:init initial-model
:update update-fn
:view view}))
Step 3: Run It
clojure -M -m myapp.core
You'll see:
Hello, TUI!
Press q to quit
Press q to exit.
Understanding the Elm Architecture
Clojure TUI uses the Elm Architecture, a pattern for building interactive applications.
The Three Parts
┌─────────────┐
│ Model │
│ (state) │
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
v │
┌─────────────┐ ┌──────┴──────┐
│ View │ │ Update │
│ (render UI) │ │(handle msg) │
└──────┬──────┘ └──────┬──────┘
│ ^
│ │
v │
┌─────────────┐ ┌──────┴──────┐
│ Screen │ │ Message │
│ (output) │ │ (input) │
└─────────────┘ └─────────────┘
-
Model: Your application state. Can be any Clojure data structure.
-
Update: A pure function that takes the current model and a message, returning a vector of
[new-model command]. -
View: A pure function that takes the model and returns a hiccup data structure representing the UI.
The Flow
- The runtime renders the initial view
- User presses a key
- The key is parsed into a message like
[:key {:char \a}] - The
updatefunction receives the model and message updatereturns a new model and optional command- The view is re-rendered with the new model
- If a command was returned, it's executed (may produce more messages)
- Repeat until
tui/quitis returned
Handling Input
Keys are represented as messages in the format [:key ...].
Key Message Examples
[:key {:char \a}] ;; Regular character
[:key {:char \Z}] ;; Uppercase character
[:key :enter] ;; Enter key
[:key :up] ;; Arrow up
[:key {:ctrl true :char \c}] ;; Ctrl+C
[:key {:alt true :char \x}] ;; Alt+X
Matching Keys
Use tui/key= to match keys in your update function:
(defn update-fn [model msg]
(cond
;; Match character 'q'
(tui/key= msg "q") [model tui/quit]
;; Match Enter key
(tui/key= msg :enter) [(handle-enter model) nil]
;; Match arrow keys
(tui/key= msg :up) [(move-up model) nil]
(tui/key= msg :down) [(move-down model) nil]
;; Match Ctrl+C
(tui/key= msg [:ctrl \c]) [model tui/quit]
;; Default
:else [model nil]))
Special Keys
| Key | Pattern |
|---|---|
| Enter | :enter |
| Escape | :escape |
| Tab | :tab |
| Backspace | :backspace |
| Arrow Up | :up |
| Arrow Down | :down |
| Arrow Left | :left |
| Arrow Right | :right |
| Home | :home |
| End | :end |
| Page Up | :page-up |
| Page Down | :page-down |
| Delete | :delete |
| Insert | :insert |
| F1-F12 | :f1 through :f12 |
Building a Counter
Let's build something more interactive: a counter.
(ns myapp.counter
(:require [tui.simple :as tui]))
(defn update-fn [model msg]
(cond
;; Quit
(tui/key= msg "q") [model tui/quit]
;; Increment with 'k' or up arrow
(or (tui/key= msg "k")
(tui/key= msg :up))
[(inc model) nil]
;; Decrement with 'j' or down arrow
(or (tui/key= msg "j")
(tui/key= msg :down))
[(dec model) nil]
;; Reset with 'r'
(tui/key= msg "r") [0 nil]
:else [model nil]))
(defn view [count]
[:col
[:box {:border :rounded :padding [0 2]}
[:row
[:text "Count: "]
[:text {:fg (cond
(pos? count) :green
(neg? count) :red
:else :yellow)
:bold true}
(str count)]]]
[:space {:height 1}]
[:text {:fg :gray} "j/k or arrows: change value"]
[:text {:fg :gray} "r: reset q: quit"]])
(defn -main [& args]
(tui/run {:init 0
:update update-fn
:view view}))
Output:
╭────────────────╮
│ Count: 0 │
╰────────────────╯
j/k or arrows: change value
r: reset q: quit
After pressing k a few times:
╭────────────────╮
│ Count: 3 │
╰────────────────╯
j/k or arrows: change value
r: reset q: quit
Commands
Commands are how your application performs side effects.
Available Commands
| Command | Description |
|---|---|
tui/quit |
Exit the application |
(tui/tick ms) |
Send :tick message after delay |
(tui/batch cmd1 cmd2 ...) |
Run commands in parallel |
(tui/sequentially cmd1 cmd2 ...) |
Run commands in sequence |
Returning Commands
Commands are returned as the second element of the update function's return vector:
(defn update-fn [model msg]
(cond
;; Return quit command
(tui/key= msg "q")
[model tui/quit]
;; Return tick command (async runtime only)
(tui/key= msg "s")
[{:started true} (tui/tick 1000)]
;; No command
:else
[model nil]))
Timer Example (Async Runtime)
(ns myapp.timer
(:require [tui.core :as tui])) ;; Note: tui.core for async
(defn update-fn [model msg]
(cond
(tui/key= msg "q")
[model tui/quit]
;; Handle tick message
(= msg :tick)
(let [new-count (dec (:count model))]
(if (pos? new-count)
[{:count new-count} (tui/tick 1000)]
[{:count 0 :done true} nil]))
:else
[model nil]))
(defn view [{:keys [count done]}]
[:col
(if done
[:text {:fg :green :bold true} "Time's up!"]
[:text {:bold true} (str "Countdown: " count)])
[:text {:fg :gray} "q: quit"]])
(defn -main [& args]
(tui/run {:init {:count 10 :done false}
:update update-fn
:view view
:init-cmd (tui/tick 1000)})) ;; Start first tick
Choosing a Runtime
tui.simple - For Babashka and Simple Apps
- Works with Babashka
- Synchronous (blocking) input
- No async commands (tick, batch, etc.)
- Lower resource usage
tui.core - For Complex Applications
- Requires full Clojure with core.async
- Async input handling
- Full command support (tick, batch, sequentially)
- Better for animations and background tasks
Configuration Options
The run function accepts these options:
| Option | Description | Default |
|---|---|---|
:init |
Initial model (required) | - |
:update |
Update function (required) | - |
:view |
View function (required) | - |
:init-cmd |
Initial command to run | nil |
:fps |
Frames per second (core only) | 60 |
:alt-screen |
Use alternate screen buffer | true |
Alternate Screen
By default, applications use the alternate screen buffer. This means:
- Your application gets a clean screen
- When you quit, the original terminal content is restored
To disable this:
(tui/run {:init model
:update update-fn
:view view
:alt-screen false})
Next Steps
- Hiccup Views - Learn about all view elements and styling
- API Reference - Complete API documentation
- Examples - Study annotated example applications