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>
This commit is contained in:
2026-01-21 11:37:16 -05:00
parent 9fe0ac2c6e
commit dab0a27e4d
5 changed files with 2712 additions and 97 deletions
+162 -97
View File
@@ -1,112 +1,186 @@
# Clojure TUI
A Clojure TUI (Terminal User Interface) framework inspired by [Bubbletea](https://github.com/charmbracelet/bubbletea), using the Elm Architecture with Hiccup for views.
A terminal user interface framework for Clojure, inspired by [Bubbletea](https://github.com/charmbracelet/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`:
```clojure
{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}
```
### Hello World
```clojure
(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
```clojure
(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
```bash
# 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
- [Getting Started](docs/getting-started.md) - Installation, first app, core concepts
- [Hiccup Views](docs/hiccup-views.md) - View elements, styling, and layout
- [API Reference](docs/api-reference.md) - Complete API documentation
- [Examples](docs/examples.md) - Annotated example applications
## Architecture
```
┌─────────────────────────────────────┐
Hiccup DSL (view returns hiccup) ← User-facing API
├─────────────────────────────────────┤
Layout Engine (calculates sizes) │ ← Constraint solving
├─────────────────────────────────────┤
│ Render (hiccup → ANSI string) │ ← Colors, styles
├─────────────────────────────────────┤
│ Terminal (raw mode, input, output) │ ← Platform abstraction
└─────────────────────────────────────┘
View (hiccup) → Render (ANSI string) → Terminal (raw mode I/O)
│ v
Model ←──────── Update ←─────────── Input (key parsing)
```
## The Elm Architecture
The application follows the Elm Architecture:
Every app has three parts:
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
```clojure
;; Model - your application state
(def initial-model {:count 0})
## Two Runtimes
;; Update - handle messages, return [new-model command]
(defn update-model [model msg]
(cond
(tui/key= msg "q") [model tui/quit]
(tui/key= msg :up) [(update model :count inc) nil]
:else [model nil]))
### `tui.simple` - Synchronous (Babashka-compatible)
;; View - render model as hiccup
(defn view [{:keys [count]}]
[:col
[:text {:bold true} "Counter"]
[:text (str "Count: " count)]
[:text {:fg :gray} "Press up to increment, q to quit"]])
```
## Hiccup Elements
| Element | Description | Attributes |
|---------|-------------|------------|
| `:text` | Styled text | `:fg` `:bg` `:bold` `:dim` `:italic` `:underline` `:inverse` |
| `:row` | Horizontal layout | `:gap` |
| `:col` | Vertical layout | `:gap` |
| `:box` | Bordered container | `:border` `:title` `:padding` |
| `:space` | Empty space | `:width` `:height` |
### Colors
`:fg` and `:bg` accept: `:black` `:red` `:green` `:yellow` `:blue` `:magenta` `:cyan` `:white` `:gray` and bright variants.
### Borders
`:border` accepts: `:rounded` `:single` `:double` `:heavy` `:ascii`
### Padding
`:padding` accepts: `n` (all sides), `[v h]` (vertical, horizontal), or `[top right bottom left]`
## Running Examples
### With Clojure CLI
```bash
# Counter - basic Elm architecture
clojure -A:dev -M -m examples.counter
# Timer - async commands (tick)
clojure -A:dev -M -m examples.timer
# List selection - cursor navigation, multi-select
clojure -A:dev -M -m examples.list-selection
# Spinner - animated loading
clojure -A:dev -M -m examples.spinner
# Views - multi-screen state machine
clojure -A:dev -M -m examples.views
# HTTP - async HTTP requests
clojure -A:dev -M -m examples.http
```
### With Babashka (limited)
The full async runtime requires `core.async`. For Babashka, use `tui.simple`:
Best for simple applications. No async support, but works with Babashka.
```clojure
(require '[tui.simple :as tui])
;; Same API, but no async commands (tick, http, etc.)
(tui/run {:init initial-model
:update update-model
:view view})
(tui/run {:init model :update update-fn :view view-fn})
```
## Built-in Commands
### `tui.core` - Asynchronous (Full Clojure)
Full-featured runtime with async commands like timers and batched operations.
```clojure
(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 program |
| `(tui/tick ms)` | Send `:tick` message after ms |
| `(tui/batch cmd1 cmd2)` | Run commands in parallel |
| `(tui/sequentially cmd1 cmd2)` | Run commands in sequence |
| `(fn [] msg)` | Custom async command |
| `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
@@ -138,15 +212,6 @@ examples/
http.clj
```
## Differences from Bubbletea
| Bubbletea (Go) | Clojure TUI |
|----------------|-------------|
| String views | Hiccup views |
| Lipgloss styling | Inline `:fg` `:bold` attrs |
| `tea.Cmd` functions | Vector commands `[:tick 100]` |
| Imperative builder | Declarative data |
## License
MIT
+723
View File
@@ -0,0 +1,723 @@
# API Reference
Complete API documentation for Clojure TUI.
## Table of Contents
- [tui.core](#tuicore) - Full async runtime
- [tui.simple](#tuisimple) - Sync runtime (Babashka)
- [tui.render](#tuirender) - Hiccup rendering
- [tui.input](#tuiinput) - Key input parsing
- [tui.terminal](#tuiterminal) - Terminal control
- [tui.ansi](#tuiansi) - ANSI escape codes
---
## tui.core
Full-featured async runtime using core.async. Use this when you need timers, async commands, or background operations.
### run
```clojure
(run options)
```
Run a TUI application synchronously (blocks until quit).
**Parameters:**
| Key | Type | Required | Description |
|-----|------|----------|-------------|
| `:init` | any | Yes | Initial model value |
| `:update` | function | Yes | `(fn [model msg] [new-model cmd])` |
| `:view` | function | Yes | `(fn [model] hiccup)` |
| `:init-cmd` | command | No | Initial command to execute |
| `:fps` | integer | No | Frames per second (default: 60) |
| `:alt-screen` | boolean | No | Use alternate screen (default: true) |
**Returns:** Final model value after quit
**Example:**
```clojure
(require '[tui.core :as tui])
(tui/run {:init {:count 0}
:update (fn [model msg]
(if (tui/key= msg "q")
[model tui/quit]
[model nil]))
:view (fn [{:keys [count]}]
[:text (str "Count: " count)])
:fps 30
:alt-screen true})
```
### quit
```clojure
quit
```
Constant value returned as a command to exit the application.
**Example:**
```clojure
(defn update-fn [model msg]
(if (tui/key= msg "q")
[model tui/quit]
[model nil]))
```
### tick
```clojure
(tick ms)
```
Create a command that sends `:tick` message after a delay.
**Parameters:**
- `ms` - Delay in milliseconds
**Returns:** A tick command
**Example:**
```clojure
;; Start a 1-second timer
(defn update-fn [model msg]
(case msg
:tick [(update model :seconds inc) (tui/tick 1000)]
[model nil]))
;; Initial tick
(tui/run {:init {:seconds 0}
:update update-fn
:view view
:init-cmd (tui/tick 1000)})
```
### batch
```clojure
(batch & cmds)
```
Create a command that executes multiple commands in parallel.
**Parameters:**
- `cmds` - Variable number of commands
**Returns:** A batch command
**Example:**
```clojure
;; Execute two async operations in parallel
(defn update-fn [model msg]
(if (= msg :start)
[model (tui/batch
(fn [] (fetch-user))
(fn [] (fetch-settings)))]
[model nil]))
```
### sequentially
```clojure
(sequentially & cmds)
```
Create a command that executes commands one after another.
**Parameters:**
- `cmds` - Variable number of commands
**Returns:** A sequential command
**Example:**
```clojure
;; First save, then notify
(defn update-fn [model msg]
(if (= msg :save)
[model (tui/sequentially
(fn [] (save-data model))
(fn [] {:type :saved}))]
[model nil]))
```
### send-msg
```clojure
(send-msg msg)
```
Create a command that immediately sends a message.
**Parameters:**
- `msg` - Message to send
**Returns:** A send-msg command
**Example:**
```clojure
(defn update-fn [model msg]
(if (tui/key= msg "r")
[{:status :resetting} (tui/send-msg :do-reset)]
[model nil]))
```
### key=
```clojure
(key= msg pattern)
```
Check if a key message matches a pattern.
**Parameters:**
- `msg` - The message to check
- `pattern` - Pattern to match (see below)
**Returns:** `true` if matches, `false` otherwise
**Pattern types:**
| Pattern | Matches |
|---------|---------|
| `"q"` | Character 'q' |
| `:enter` | Enter key |
| `:up` | Arrow up |
| `[:ctrl \c]` | Ctrl+C |
| `[:alt \x]` | Alt+X |
**Example:**
```clojure
(defn update-fn [model msg]
(cond
(tui/key= msg "q") [model tui/quit]
(tui/key= msg :up) [(update model :y dec) nil]
(tui/key= msg [:ctrl \c]) [model tui/quit]
:else [model nil]))
```
### key-str
```clojure
(key-str msg)
```
Convert a key message to a human-readable string.
**Parameters:**
- `msg` - Key message
**Returns:** String representation
**Example:**
```clojure
(tui/key-str [:key {:char \a}]) ;; => "a"
(tui/key-str [:key :up]) ;; => "up"
(tui/key-str [:key {:ctrl true :char \c}]) ;; => "ctrl+c"
```
### render
```clojure
(render hiccup)
```
Re-exported from `tui.render`. Render hiccup to ANSI string.
---
## tui.simple
Synchronous runtime compatible with Babashka. Same API as `tui.core` but without async commands.
### run
```clojure
(run options)
```
Run a TUI application synchronously.
**Parameters:**
| Key | Type | Required | Description |
|-----|------|----------|-------------|
| `:init` | any | Yes | Initial model value |
| `:update` | function | Yes | `(fn [model msg] [new-model cmd])` |
| `:view` | function | Yes | `(fn [model] hiccup)` |
| `:alt-screen` | boolean | No | Use alternate screen (default: true) |
**Returns:** Final model value after quit
**Example:**
```clojure
(require '[tui.simple :as tui])
(tui/run {:init 0
:update (fn [model msg]
(cond
(tui/key= msg "q") [model tui/quit]
(tui/key= msg "k") [(inc model) nil]
:else [model nil]))
:view (fn [count]
[:text (str "Count: " count)])})
```
### quit
Same as `tui.core/quit`.
### key=
Same as `tui.core/key=`.
### key-str
Same as `tui.core/key-str`.
### render
Re-exported from `tui.render`.
---
## tui.render
Converts hiccup data structures to ANSI-formatted strings.
### render
```clojure
(render hiccup)
(render hiccup ctx)
```
Render hiccup to ANSI string.
**Parameters:**
- `hiccup` - Hiccup data structure
- `ctx` - Optional context map (for internal use)
**Returns:** String with ANSI escape codes
**Example:**
```clojure
(require '[tui.render :as r])
(r/render [:text {:fg :red :bold true} "Error!"])
;; => "\e[31m\e[1mError!\e[0m"
(r/render [:col
[:text "Line 1"]
[:text "Line 2"]])
;; => "Line 1\nLine 2"
(r/render [:box {:border :rounded} "Content"])
;; => "╭─────────╮\n│Content │\n╰─────────╯"
```
### text
```clojure
(text & args)
```
Helper function to create `:text` elements.
**Example:**
```clojure
(text "Hello") ;; => [:text "Hello"]
(text {:fg :red} "Error") ;; => [:text {:fg :red} "Error"]
```
### row
```clojure
(row & args)
```
Helper function to create `:row` elements.
**Example:**
```clojure
(row "A" "B" "C") ;; => [:row "A" "B" "C"]
(row {:gap 2} "A" "B") ;; => [:row {:gap 2} "A" "B"]
```
### col
```clojure
(col & args)
```
Helper function to create `:col` elements.
**Example:**
```clojure
(col "Line 1" "Line 2") ;; => [:col "Line 1" "Line 2"]
(col {:gap 1} "A" "B") ;; => [:col {:gap 1} "A" "B"]
```
### box
```clojure
(box & args)
```
Helper function to create `:box` elements.
**Example:**
```clojure
(box "Content") ;; => [:box "Content"]
(box {:title "Info"} "Content") ;; => [:box {:title "Info"} "Content"]
```
---
## tui.input
Parses raw terminal input into structured key messages.
### read-key
```clojure
(read-key)
```
Read a single key event from the terminal.
**Returns:** Key message vector `[:key ...]`
**Key message formats:**
```clojure
;; Regular character
[:key {:char \a}]
;; Special key
[:key :enter]
[:key :up]
[:key :f1]
;; Control combination
[:key {:ctrl true :char \c}]
;; Alt combination
[:key {:alt true :char \x}]
;; Unknown sequence
[:key :unknown "\e[xyz"]
```
### Special Key Keywords
| Keyword | Key |
|---------|-----|
| `:up` | Arrow Up |
| `:down` | Arrow Down |
| `:left` | Arrow Left |
| `:right` | Arrow Right |
| `:home` | Home |
| `:end` | End |
| `:page-up` | Page Up |
| `:page-down` | Page Down |
| `:insert` | Insert |
| `:delete` | Delete |
| `:escape` | Escape |
| `:tab` | Tab |
| `:shift-tab` | Shift+Tab |
| `:enter` | Enter |
| `:backspace` | Backspace |
| `:f1` - `:f12` | Function keys |
### key-match?
```clojure
(key-match? msg pattern)
```
Internal function used by `key=` to match patterns.
### key->str
```clojure
(key->str msg)
```
Convert key message to string representation.
**Example:**
```clojure
(key->str [:key {:char \a}]) ;; => "a"
(key->str [:key :enter]) ;; => "enter"
(key->str [:key {:ctrl true :char \c}]) ;; => "ctrl+c"
(key->str [:key {:alt true :char \x}]) ;; => "alt+x"
```
---
## tui.terminal
Low-level terminal control functions.
### get-terminal-size
```clojure
(get-terminal-size)
```
Get the terminal dimensions.
**Returns:** Map with `:width` and `:height` keys
**Example:**
```clojure
(require '[tui.terminal :as term])
(term/get-terminal-size)
;; => {:width 120 :height 40}
```
### raw-mode!
```clojure
(raw-mode!)
```
Enter raw terminal mode. Disables echo and line buffering.
### restore!
```clojure
(restore!)
```
Restore terminal to original state.
### alt-screen!
```clojure
(alt-screen!)
```
Enter the alternate screen buffer.
### exit-alt-screen!
```clojure
(exit-alt-screen!)
```
Exit the alternate screen buffer.
### clear!
```clojure
(clear!)
```
Clear the screen and move cursor to home position.
### render!
```clojure
(render! s)
```
Render a string to the terminal.
**Parameters:**
- `s` - String to render (typically from `tui.render/render`)
### Input Functions
```clojure
(init-input!) ;; Initialize input reader
(close-input!) ;; Close input reader
(input-ready?) ;; Check if input available (non-blocking)
(read-char) ;; Read single character (blocking)
(read-available) ;; Read all available characters
(read-char-timeout ms) ;; Read with timeout
```
---
## tui.ansi
ANSI escape codes and text styling utilities.
### style
```clojure
(style text & {:keys [fg bg bold dim italic underline inverse strike]})
```
Apply multiple styles to text.
**Parameters:**
- `text` - String to style
- `:fg` - Foreground color
- `:bg` - Background color
- `:bold` - Boolean
- `:dim` - Boolean
- `:italic` - Boolean
- `:underline` - Boolean
- `:inverse` - Boolean
- `:strike` - Boolean
**Example:**
```clojure
(require '[tui.ansi :as ansi])
(ansi/style "Error" :fg :red :bold true)
(ansi/style "Warning" :fg :yellow :bg :black :underline true)
```
### Color Functions
```clojure
(fg color text) ;; Set foreground color
(bg color text) ;; Set background color
(fg-256 n text) ;; 256-color foreground (0-255)
(bg-256 n text) ;; 256-color background (0-255)
(fg-rgb r g b text) ;; True color foreground (24-bit)
(bg-rgb r g b text) ;; True color background (24-bit)
```
**Example:**
```clojure
(ansi/fg :red "Error")
(ansi/bg :yellow "Highlighted")
(ansi/fg-256 208 "Orange")
(ansi/fg-rgb 255 128 0 "True color orange")
```
### String Utilities
```clojure
(visible-length s) ;; Get visible length (excludes ANSI codes)
(pad-right s width) ;; Pad with spaces on right
(pad-left s width) ;; Pad with spaces on left
(pad-center s width) ;; Center within width
(truncate s max-width) ;; Truncate with ellipsis
```
**Example:**
```clojure
(ansi/visible-length "\e[31mRed\e[0m") ;; => 3
(ansi/pad-right "Hi" 10) ;; => "Hi "
(ansi/pad-center "Hi" 10) ;; => " Hi "
(ansi/truncate "Hello World" 8) ;; => "Hello..."
```
### Cursor Control
```clojure
(cursor-to row col) ;; Move cursor to position (1-indexed)
(cursor-up n) ;; Move cursor up n lines
(cursor-down n) ;; Move cursor down n lines
(cursor-forward n) ;; Move cursor right n columns
(cursor-back n) ;; Move cursor left n columns
```
### Constants
```clojure
clear-screen ;; Clear entire screen
clear-line ;; Clear current line
clear-to-end ;; Clear from cursor to end of screen
cursor-home ;; Move cursor to home (1,1)
hide-cursor ;; Hide cursor
show-cursor ;; Show cursor
enter-alt-screen ;; Enter alternate screen buffer
exit-alt-screen ;; Exit alternate screen buffer
cursor-save ;; Save cursor position
cursor-restore ;; Restore cursor position
reset ;; Reset all attributes
```
### Box Drawing Characters
```clojure
ansi/box-chars
;; => {:rounded {:tl "╭" :tr "╮" :bl "╰" :br "╯" :h "─" :v "│"}
;; :single {:tl "┌" :tr "┐" :bl "└" :br "┘" :h "─" :v "│"}
;; :double {:tl "╔" :tr "╗" :bl "╚" :br "╝" :h "═" :v "║"}
;; :heavy {:tl "┏" :tr "┓" :bl "┗" :br "┛" :h "━" :v "┃"}
;; :ascii {:tl "+" :tr "+" :bl "+" :br "+" :h "-" :v "|"}}
```
### Color Maps
```clojure
ansi/fg-colors ;; Map of color keywords to ANSI codes
ansi/bg-colors ;; Map of color keywords to ANSI codes
ansi/attrs ;; Map of attribute keywords to ANSI codes
```
---
## Custom Async Commands
In `tui.core`, you can create custom async commands by returning functions:
```clojure
;; Custom command that fetches data
(defn fetch-data-cmd []
(fn []
;; This runs asynchronously
(let [result (http/get "https://api.example.com/data")]
{:type :data-loaded :data (:body result)})))
(defn update-fn [model msg]
(cond
(tui/key= msg "f")
[{:loading true} (fetch-data-cmd)]
(= (:type msg) :data-loaded)
[{:loading false :data (:data msg)} nil]
:else
[model nil]))
```
The function must:
1. Take no arguments
2. Return a message (any Clojure value)
3. The returned message will be passed to your update function
---
## Summary
| Namespace | Purpose |
|-----------|---------|
| `tui.core` | Full async runtime with all features |
| `tui.simple` | Sync runtime for Babashka |
| `tui.render` | Hiccup to ANSI rendering |
| `tui.input` | Key input parsing |
| `tui.terminal` | Low-level terminal control |
| `tui.ansi` | ANSI codes and text utilities |
+905
View File
@@ -0,0 +1,905 @@
# Examples
Detailed walkthroughs of the example applications included with Clojure TUI.
## Running Examples
```bash
# 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
clojure -A:dev -M -m examples.timer
clojure -A:dev -M -m examples.list-selection
clojure -A:dev -M -m examples.spinner
clojure -A:dev -M -m examples.views
clojure -A:dev -M -m examples.http
```
---
## Counter
A simple counter demonstrating the basic Elm architecture.
**Features:**
- Basic state management
- Key handling (multiple keys for same action)
- Conditional styling based on value
- Box layout
### Code
```clojure
(ns examples.counter
(:require [tui.core :as tui]))
(defn update-fn [model msg]
(cond
;; Quit on 'q' or Ctrl+C
(or (tui/key= msg "q")
(tui/key= msg [:ctrl \c]))
[model tui/quit]
;; Increment on 'k' or up arrow
(or (tui/key= msg "k")
(tui/key= msg :up))
[(inc model) nil]
;; Decrement on 'j' or down arrow
(or (tui/key= msg "j")
(tui/key= msg :down))
[(dec model) nil]
;; Reset on 'r'
(tui/key= msg "r")
[0 nil]
:else
[model nil]))
(defn view [count]
[:col
[:box {:border :rounded :padding [0 2]}
[:text {:fg (cond
(pos? count) :green
(neg? count) :red
:else :yellow)
:bold true}
(str "Counter: " count)]]
[:space {:height 1}]
[:text {:fg :gray} "j/k or up/down: change value"]
[:text {:fg :gray} "r: reset q: quit"]])
(defn -main [& args]
(tui/run {:init 0
:update update-fn
:view view}))
```
### Output
Initial state:
```
╭────────────────╮
│ Counter: 0 │
╰────────────────╯
j/k or up/down: change value
r: reset q: quit
```
After pressing `k` three times:
```
╭────────────────╮
│ Counter: 3 │
╰────────────────╯
j/k or up/down: change value
r: reset q: quit
```
After pressing `j` five times (negative):
```
╭────────────────╮
│ Counter: -2 │
╰────────────────╯
j/k or up/down: change value
r: reset q: quit
```
### Key Concepts
1. **Multiple key bindings**: Use `or` to bind multiple keys to the same action
2. **Conditional styling**: Change colors based on state (`pos?`, `neg?`)
3. **Simple state**: Model is just an integer
---
## Timer
A countdown timer demonstrating async commands with `tick`.
**Features:**
- Async tick command
- State machine (running/paused/done)
- Conditional rendering
- Pause/resume functionality
### Code
```clojure
(ns examples.timer
(:require [tui.core :as tui]))
(def initial-seconds 10)
(defn update-fn [model msg]
(cond
;; Quit
(tui/key= msg "q")
[model tui/quit]
;; Toggle pause with space
(tui/key= msg " ")
(if (:done model)
[model nil]
(let [paused (not (:paused model))]
[(assoc model :paused paused)
(when-not paused (tui/tick 1000))]))
;; Reset with 'r'
(tui/key= msg "r")
[{:seconds initial-seconds :paused false :done false}
(tui/tick 1000)]
;; Handle tick
(= msg :tick)
(if (:paused model)
[model nil]
(let [new-seconds (dec (:seconds model))]
(if (pos? new-seconds)
[{:seconds new-seconds :paused false :done false}
(tui/tick 1000)]
[{:seconds 0 :paused false :done true} nil])))
:else
[model nil]))
(defn format-time [seconds]
(let [mins (quot seconds 60)
secs (mod seconds 60)]
(format "%02d:%02d" mins secs)))
(defn view [{:keys [seconds paused done]}]
[:col
[:box {:border :rounded :padding [0 2]}
[:col
[:text {:bold true} "Countdown Timer"]
[:space {:height 1}]
[:text {:fg (cond
done :green
paused :yellow
:else :cyan)
:bold true}
(if done
"Time's up!"
(format-time seconds))]
(when paused
[:text {:fg :yellow} "(paused)"])]]
[:space {:height 1}]
[:text {:fg :gray} "space: pause/resume r: reset q: quit"]])
(defn -main [& args]
(tui/run {:init {:seconds initial-seconds :paused false :done false}
:update update-fn
:view view
:init-cmd (tui/tick 1000)}))
```
### Output
Running:
```
╭──────────────────────╮
│ Countdown Timer │
│ │
│ 00:07 │
╰──────────────────────╯
space: pause/resume r: reset q: quit
```
Paused:
```
╭──────────────────────╮
│ Countdown Timer │
│ │
│ 00:05 │
│ (paused) │
╰──────────────────────╯
space: pause/resume r: reset q: quit
```
Done:
```
╭──────────────────────╮
│ Countdown Timer │
│ │
│ Time's up! │
╰──────────────────────╯
space: pause/resume r: reset q: quit
```
### Key Concepts
1. **Tick command**: `(tui/tick 1000)` sends `:tick` after 1 second
2. **Initial command**: `:init-cmd` starts the first tick
3. **State machine**: Track `paused` and `done` states
4. **Conditional ticks**: Only schedule next tick when not paused
---
## List Selection
A multi-select list demonstrating cursor navigation and selection.
**Features:**
- Cursor navigation with bounds checking
- Multi-select using a set
- Visual selection indicators
- Enter to confirm
### Code
```clojure
(ns examples.list-selection
(:require [tui.simple :as tui]))
(def items
["Apple" "Banana" "Cherry" "Date" "Elderberry"
"Fig" "Grape" "Honeydew"])
(defn update-fn [model msg]
(cond
;; Quit
(tui/key= msg "q")
[model tui/quit]
;; Move up
(or (tui/key= msg "k")
(tui/key= msg :up))
[(update model :cursor #(max 0 (dec %))) nil]
;; Move down
(or (tui/key= msg "j")
(tui/key= msg :down))
[(update model :cursor #(min (dec (count items)) (inc %))) nil]
;; Toggle selection
(tui/key= msg " ")
(let [item (nth items (:cursor model))]
[(update model :selected
#(if (contains? % item)
(disj % item)
(conj % item)))
nil])
;; Confirm and quit
(tui/key= msg :enter)
[(assoc model :confirmed true) tui/quit]
:else
[model nil]))
(defn view [{:keys [cursor selected]}]
[:col
[:text {:bold true} "Select items:"]
[:space {:height 1}]
[:col
(for [[idx item] (map-indexed vector items)]
(let [is-cursor (= idx cursor)
is-selected (contains? selected item)]
[:row {:gap 1}
[:text (if is-cursor ">" " ")]
[:text (if is-selected "[x]" "[ ]")]
[:text {:fg (cond
is-cursor :cyan
is-selected :green
:else :white)}
item]]))]
[:space {:height 1}]
[:text {:fg :gray} "j/k: move space: toggle enter: confirm q: quit"]])
(defn -main [& args]
(let [result (tui/run {:init {:cursor 0 :selected #{}}
:update update-fn
:view view})]
(when (:confirmed result)
(println "Selected:" (:selected result)))))
```
### Output
Initial state (cursor on Apple):
```
Select items:
> [ ] Apple
[ ] Banana
[ ] Cherry
[ ] Date
[ ] Elderberry
[ ] Fig
[ ] Grape
[ ] Honeydew
j/k: move space: toggle enter: confirm q: quit
```
After selecting some items:
```
Select items:
[x] Apple
> [ ] Banana
[x] Cherry
[ ] Date
[ ] Elderberry
[ ] Fig
[x] Grape
[ ] Honeydew
j/k: move space: toggle enter: confirm q: quit
```
### Key Concepts
1. **Cursor bounds**: Use `max` and `min` to keep cursor in range
2. **Set for selection**: Using a set makes toggle logic simple
3. **Result after quit**: Access final model after `run` returns
4. **Visual indicators**: Different characters for cursor (`>`) and selection (`[x]`)
---
## Spinner
An animated loading spinner demonstrating fast tick-based animation.
**Features:**
- Multiple spinner styles
- Fast animation (80ms ticks)
- Tab to cycle styles
- State machine (loading/done)
### Code
```clojure
(ns examples.spinner
(:require [tui.core :as tui]))
(def spinners
{:dots {:frames ["⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏"] :interval 80}
:line {:frames ["-" "\\" "|" "/"] :interval 80}
:circle {:frames ["◐" "◓" "◑" "◒"] :interval 100}
:square {:frames ["◰" "◳" "◲" "◱"] :interval 100}
:triangle {:frames ["◢" "◣" "◤" "◥"] :interval 80}
:bounce {:frames ["⠁" "⠂" "⠄" "⠂"] :interval 120}
:dots2 {:frames ["⣾" "⣽" "⣻" "⢿" "⡿" "⣟" "⣯" "⣷"] :interval 80}
:arc {:frames ["◜" "◠" "◝" "◞" "◡" "◟"] :interval 100}
:toggle {:frames ["⊶" "⊷"] :interval 250}
:arrow {:frames ["←" "↖" "↑" "↗" "→" "↘" "↓" "↙"] :interval 100}})
(def spinner-order (vec (keys spinners)))
(defn update-fn [model msg]
(cond
;; Quit
(tui/key= msg "q")
[model tui/quit]
;; Cycle spinner style with Tab
(tui/key= msg :tab)
(let [next-idx (mod (inc (:style-idx model)) (count spinner-order))]
[(assoc model :style-idx next-idx :frame 0) nil])
;; Complete with space
(tui/key= msg " ")
[(assoc model :done true) nil]
;; Reset with 'r'
(tui/key= msg "r")
[{:style-idx (:style-idx model) :frame 0 :done false}
(tui/tick 80)]
;; Handle tick
(= msg :tick)
(if (:done model)
[model nil]
(let [style-key (nth spinner-order (:style-idx model))
frames (get-in spinners [style-key :frames])
interval (get-in spinners [style-key :interval])
next-frame (mod (inc (:frame model)) (count frames))]
[(assoc model :frame next-frame)
(tui/tick interval)]))
:else
[model nil]))
(defn view [{:keys [style-idx frame done]}]
(let [style-key (nth spinner-order style-idx)
spinner (get spinners style-key)
char (nth (:frames spinner) frame)]
[:col
[:box {:border :rounded :padding [0 2]}
[:col
[:text {:bold true} (str "Spinner: " (name style-key))]
[:space {:height 1}]
(if done
[:row
[:text {:fg :green :bold true} "✓"]
[:text " Done!"]]
[:row
[:text {:fg :cyan :bold true} char]
[:text " Loading..."]])]]
[:space {:height 1}]
[:text {:fg :gray} "tab: change style space: complete r: restart q: quit"]]))
(defn -main [& args]
(tui/run {:init {:style-idx 0 :frame 0 :done false}
:update update-fn
:view view
:init-cmd (tui/tick 80)}))
```
### Output
Loading (with dots spinner):
```
╭─────────────────────────╮
│ Spinner: dots │
│ │
│ ⠹ Loading... │
╰─────────────────────────╯
tab: change style space: complete r: restart q: quit
```
Loading (with arrow spinner):
```
╭─────────────────────────╮
│ Spinner: arrow │
│ │
│ → Loading... │
╰─────────────────────────╯
tab: change style space: complete r: restart q: quit
```
Done:
```
╭─────────────────────────╮
│ Spinner: dots │
│ │
│ ✓ Done! │
╰─────────────────────────╯
tab: change style space: complete r: restart q: quit
```
### Key Concepts
1. **Fast ticks**: Use short intervals (80-100ms) for smooth animation
2. **Frame-based animation**: Cycle through an array of frames
3. **Multiple styles**: Store different spinner configurations
4. **Stop animation**: Stop scheduling ticks when done
---
## Views (Multi-Screen)
A multi-view application demonstrating state machine navigation.
**Features:**
- Multiple views (menu, detail, confirm dialog)
- Navigation between views
- Confirmation dialog for quit
- Different box styles per view
### Code
```clojure
(ns examples.views
(:require [tui.simple :as tui]))
(def menu-items
[{:id :profile :label "View Profile" :desc "See your user information"}
{:id :settings :label "Settings" :desc "Configure preferences"}
{:id :help :label "Help" :desc "Get assistance"}])
(defn handle-menu [model msg]
(cond
(tui/key= msg "q")
[(assoc model :view :confirm) nil]
(or (tui/key= msg "k") (tui/key= msg :up))
[(update model :cursor #(max 0 (dec %))) nil]
(or (tui/key= msg "j") (tui/key= msg :down))
[(update model :cursor #(min (dec (count menu-items)) (inc %))) nil]
(tui/key= msg :enter)
(let [item (nth menu-items (:cursor model))]
[(assoc model :view :detail :selected item) nil])
:else
[model nil]))
(defn handle-detail [model msg]
(cond
(or (tui/key= msg "b")
(tui/key= msg :escape))
[(assoc model :view :menu) nil]
(tui/key= msg "q")
[(assoc model :view :confirm) nil]
:else
[model nil]))
(defn handle-confirm [model msg]
(cond
(tui/key= msg "y")
[model tui/quit]
(or (tui/key= msg "n")
(tui/key= msg :escape))
[(assoc model :view :menu) nil]
:else
[model nil]))
(defn update-fn [model msg]
(case (:view model)
:menu (handle-menu model msg)
:detail (handle-detail model msg)
:confirm (handle-confirm model msg)))
(defn menu-view [{:keys [cursor]}]
[:col
[:box {:border :rounded :title "Main Menu" :padding 1}
[:col
(for [[idx item] (map-indexed vector menu-items)]
(let [selected (= idx cursor)]
[:row
[:text (if selected "> " " ")]
[:text {:fg (if selected :cyan :white)
:bold selected}
(:label item)]]))]]
[:space {:height 1}]
[:text {:fg :gray} "j/k: navigate enter: select q: quit"]])
(defn detail-view [{:keys [selected]}]
[:col
[:box {:border :double :title (:label selected) :padding 1}
[:col
[:text {:bold true} "Description:"]
[:text (:desc selected)]
[:space {:height 1}]
[:text {:fg :gray :dim true} (str "ID: " (name (:id selected)))]]]
[:space {:height 1}]
[:text {:fg :gray} "b/esc: back q: quit"]])
(defn confirm-view [model]
[:col
[:box {:border :rounded :title "Confirm" :padding 1}
[:col
[:text "Are you sure you want to quit?"]
[:space {:height 1}]
[:row {:gap 2}
[:text {:fg :green} "[Y]es"]
[:text {:fg :red} "[N]o"]]]]
[:space {:height 1}]
[:text {:fg :gray} "y: quit n: cancel"]])
(defn view [model]
(case (:view model)
:menu (menu-view model)
:detail (detail-view model)
:confirm (confirm-view model)))
(defn -main [& args]
(tui/run {:init {:view :menu :cursor 0 :selected nil}
:update update-fn
:view view}))
```
### Output
Menu view:
```
╭─Main Menu─────────────────────╮
│ │
│ > View Profile │
│ Settings │
│ Help │
│ │
╰───────────────────────────────╯
j/k: navigate enter: select q: quit
```
Detail view (Profile selected):
```
╔═View Profile══════════════════╗
║ ║
║ Description: ║
║ See your user information ║
║ ║
║ ID: profile ║
║ ║
╚═══════════════════════════════╝
b/esc: back q: quit
```
Confirm dialog:
```
╭─Confirm───────────────────────╮
│ │
│ Are you sure you want to │
│ quit? │
│ │
│ [Y]es [N]o │
│ │
╰───────────────────────────────╯
y: quit n: cancel
```
### Key Concepts
1. **State machine pattern**: Use `:view` key to track current screen
2. **Separate handlers**: Break update function into per-view handlers
3. **View dispatch**: Use `case` in both update and view functions
4. **Confirmation dialog**: Intercept quit to show confirmation
---
## HTTP (Async Requests)
An async HTTP request example demonstrating custom commands.
**Features:**
- Custom async command function
- State machine (idle/loading/success/error)
- Error handling
- URL input display
### Code
```clojure
(ns examples.http
(:require [tui.core :as tui]
[clj-http.client :as http]))
(def api-url "https://httpbin.org/get")
(defn fetch-data []
(fn []
(try
(let [response (http/get api-url {:as :json})]
{:type :success :data (:body response)})
(catch Exception e
{:type :error :message (.getMessage e)}))))
(defn update-fn [model msg]
(cond
;; Quit
(tui/key= msg "q")
[model tui/quit]
;; Start fetch
(tui/key= msg :enter)
(if (= (:status model) :loading)
[model nil]
[{:status :loading} (fetch-data)])
;; Reset
(tui/key= msg "r")
[{:status :idle} nil]
;; Handle success
(= (:type msg) :success)
[{:status :success :data (:data msg)} nil]
;; Handle error
(= (:type msg) :error)
[{:status :error :error (:message msg)} nil]
:else
[model nil]))
(defn view [{:keys [status data error]}]
[:col
[:box {:border :rounded :title "HTTP Example" :padding 1}
[:col
[:row
[:text {:bold true} "URL: "]
[:text {:fg :cyan} api-url]]
[:space {:height 1}]
[:row
[:text {:bold true} "Status: "]
[:text {:fg (case status
:idle :gray
:loading :yellow
:success :green
:error :red)}
(name status)]]
[:space {:height 1}]
(case status
:idle
[:text {:fg :gray} "Press Enter to fetch data"]
:loading
[:text {:fg :yellow} "Fetching..."]
:success
[:col
[:text {:fg :green} "Response received!"]
[:text {:fg :gray :dim true}
(str "Origin: " (get data "origin"))]]
:error
[:col
[:text {:fg :red} "Request failed"]
[:text {:fg :red :dim true} error]])]]
[:space {:height 1}]
[:text {:fg :gray} "enter: fetch r: reset q: quit"]])
(defn -main [& args]
(tui/run {:init {:status :idle}
:update update-fn
:view view}))
```
### Output
Idle state:
```
╭─HTTP Example──────────────────────────╮
│ │
│ URL: https://httpbin.org/get │
│ │
│ Status: idle │
│ │
│ Press Enter to fetch data │
│ │
╰───────────────────────────────────────╯
enter: fetch r: reset q: quit
```
Loading state:
```
╭─HTTP Example──────────────────────────╮
│ │
│ URL: https://httpbin.org/get │
│ │
│ Status: loading │
│ │
│ Fetching... │
│ │
╰───────────────────────────────────────╯
enter: fetch r: reset q: quit
```
Success state:
```
╭─HTTP Example──────────────────────────╮
│ │
│ URL: https://httpbin.org/get │
│ │
│ Status: success │
│ │
│ Response received! │
│ Origin: 203.0.113.42 │
│ │
╰───────────────────────────────────────╯
enter: fetch r: reset q: quit
```
Error state:
```
╭─HTTP Example──────────────────────────╮
│ │
│ URL: https://httpbin.org/get │
│ │
│ Status: error │
│ │
│ Request failed │
│ Connection refused │
│ │
╰───────────────────────────────────────╯
enter: fetch r: reset q: quit
```
### Key Concepts
1. **Custom async command**: Return a function `(fn [] result)`
2. **State machine**: Track loading/success/error states
3. **Error handling**: Wrap async operation in try/catch
4. **Message types**: Use maps with `:type` key for complex messages
---
## Common Patterns
### Pattern: State Machine
```clojure
(defn update-fn [model msg]
(case (:state model)
:menu (handle-menu model msg)
:game (handle-game model msg)
:paused (handle-paused model msg)
:gameover (handle-gameover model msg)))
```
### Pattern: Cursor Navigation
```clojure
(defn move-up [model]
(update model :cursor #(max 0 (dec %))))
(defn move-down [model items]
(update model :cursor #(min (dec (count items)) (inc %))))
```
### Pattern: Toggle Selection
```clojure
(defn toggle [model item]
(update model :selected
#(if (contains? % item)
(disj % item)
(conj % item))))
```
### Pattern: Confirmation Dialog
```clojure
(defn update-fn [model msg]
(if (:confirming model)
(cond
(tui/key= msg "y") [model tui/quit]
(tui/key= msg "n") [(assoc model :confirming false) nil]
:else [model nil])
(if (tui/key= msg "q")
[(assoc model :confirming true) nil]
;; ... normal handling
)))
```
### Pattern: Loading States
```clojure
(defn view [{:keys [loading error data]}]
(cond
loading [:text "Loading..."]
error [:text {:fg :red} error]
data [:text (str "Data: " data)]
:else [:text "Press Enter to load"]))
```
+380
View File
@@ -0,0 +1,380 @@
# 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`:
```clojure
{: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
```bash
mkdir my-tui-app
cd my-tui-app
```
Create a `deps.edn`:
```clojure
{:deps {io.github.yourname/clojure-tui {:git/tag "v0.1.0" :git/sha "..."}}}
```
### Step 2: Write the Application
Create `src/myapp/core.clj`:
```clojure
(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
```bash
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](https://guide.elm-lang.org/architecture/), a pattern for building interactive applications.
### The Three Parts
```
┌─────────────┐
│ Model │
│ (state) │
└──────┬──────┘
┌────────────┴────────────┐
│ │
v │
┌─────────────┐ ┌──────┴──────┐
│ View │ │ Update │
│ (render UI) │ │(handle msg) │
└──────┬──────┘ └──────┬──────┘
│ ^
│ │
v │
┌─────────────┐ ┌──────┴──────┐
│ Screen │ │ Message │
│ (output) │ │ (input) │
└─────────────┘ └─────────────┘
```
1. **Model**: Your application state. Can be any Clojure data structure.
2. **Update**: A pure function that takes the current model and a message, returning a vector of `[new-model command]`.
3. **View**: A pure function that takes the model and returns a hiccup data structure representing the UI.
### The Flow
1. The runtime renders the initial view
2. User presses a key
3. The key is parsed into a message like `[:key {:char \a}]`
4. The `update` function receives the model and message
5. `update` returns a new model and optional command
6. The view is re-rendered with the new model
7. If a command was returned, it's executed (may produce more messages)
8. Repeat until `tui/quit` is returned
## Handling Input
Keys are represented as messages in the format `[:key ...]`.
### Key Message Examples
```clojure
[: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:
```clojure
(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.
```clojure
(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:
```clojure
(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)
```clojure
(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:
```clojure
(tui/run {:init model
:update update-fn
:view view
:alt-screen false})
```
## Next Steps
- [Hiccup Views](hiccup-views.md) - Learn about all view elements and styling
- [API Reference](api-reference.md) - Complete API documentation
- [Examples](examples.md) - Study annotated example applications
+542
View File
@@ -0,0 +1,542 @@
# Hiccup Views
Clojure TUI uses Hiccup-style syntax for declarative UI definitions. Views are pure functions that return nested data structures representing the UI.
## Basic Syntax
Views use vectors with keywords as element tags:
```clojure
[:element-type {attributes} children...]
```
Examples:
```clojure
[:text "Hello"] ;; Simple text
[:text {:fg :red} "Error"] ;; Text with attributes
[:col [:text "Line 1"] [:text "Line 2"]] ;; Nested elements
```
## Elements
### `:text` - Styled Text
The basic building block for displaying text.
```clojure
;; Simple text
[:text "Hello, World!"]
;; Styled text
[:text {:fg :cyan :bold true} "Important"]
;; Multiple style attributes
[:text {:fg :white :bg :red :bold true :underline true} "Alert!"]
```
**Output:**
```
Hello, World!
Important
Alert!
```
**Attributes:**
| Attribute | Type | Description |
|-----------|------|-------------|
| `:fg` | keyword/int | Foreground color |
| `:bg` | keyword/int | Background color |
| `:bold` | boolean | Bold text |
| `:dim` | boolean | Dimmed text |
| `:italic` | boolean | Italic text |
| `:underline` | boolean | Underlined text |
| `:inverse` | boolean | Swap fg/bg colors |
| `:strike` | boolean | Strikethrough text |
### `:row` - Horizontal Layout
Arranges children horizontally (left to right).
```clojure
;; Basic row
[:row "Left" "Middle" "Right"]
;; Row with gap
[:row {:gap 2} "A" "B" "C"]
;; Nested elements in row
[:row
[:text {:fg :green} "Status:"]
[:text {:bold true} "OK"]]
```
**Output:**
```
LeftMiddleRight
A B C
Status:OK
```
**Attributes:**
| Attribute | Type | Description |
|-----------|------|-------------|
| `:gap` | integer | Spaces between children (default: 0) |
### `:col` - Vertical Layout
Arranges children vertically (top to bottom).
```clojure
;; Basic column
[:col "Line 1" "Line 2" "Line 3"]
;; Column with gap
[:col {:gap 1}
[:text "Section 1"]
[:text "Section 2"]]
;; Nested layouts
[:col
[:text {:bold true} "Header"]
[:row "Col A" "Col B" "Col C"]
[:text {:fg :gray} "Footer"]]
```
**Output:**
```
Line 1
Line 2
Line 3
Section 1
Section 2
Header
Col ACol BCol C
Footer
```
**Attributes:**
| Attribute | Type | Description |
|-----------|------|-------------|
| `:gap` | integer | Blank lines between children (default: 0) |
### `:box` - Bordered Container
Wraps content in a bordered box.
```clojure
;; Simple box
[:box "Content"]
;; Box with title
[:box {:title "Settings"} "Options go here"]
;; Box with padding
[:box {:padding 1} "Padded content"]
;; Box with custom border style
[:box {:border :double} "Important!"]
```
**Output:**
```
╭─────────╮
│Content │
╰─────────╯
╭─Settings─╮
│Options go here│
╰──────────╯
╭──────────────────╮
│ │
│ Padded content │
│ │
╰──────────────────╯
╔═══════════╗
║Important! ║
╚═══════════╝
```
**Attributes:**
| Attribute | Type | Description |
|-----------|------|-------------|
| `:border` | keyword | Border style (see below) |
| `:title` | string | Title in top border |
| `:padding` | int/vec | Inner padding (see below) |
| `:width` | integer | Fixed width |
**Border Styles:**
| Style | Characters | Example |
|-------|------------|---------|
| `:rounded` | `╭╮╰╯─│` | `╭───╮` (default) |
| `:single` | `┌┐└┘─│` | `┌───┐` |
| `:double` | `╔╗╚╝═║` | `╔═══╗` |
| `:heavy` | `┏┓┗┛━┃` | `┏━━━┓` |
| `:ascii` | `++--\|` | `+---+` |
**Padding:**
```clojure
;; All sides
[:box {:padding 2} "Content"]
;; Vertical and horizontal [v h]
[:box {:padding [1 2]} "Content"]
;; Individual [top right bottom left]
[:box {:padding [1 2 1 2]} "Content"]
```
### `:space` - Empty Space
Creates empty space for layout purposes.
```clojure
;; Default 1x1 space
[:space]
;; Horizontal space
[:row "Left" [:space {:width 10}] "Right"]
;; Vertical space
[:col "Top" [:space {:height 3}] "Bottom"]
```
**Output:**
```
Left Right
Top
Bottom
```
**Attributes:**
| Attribute | Type | Description |
|-----------|------|-------------|
| `:width` | integer | Width in characters (default: 1) |
| `:height` | integer | Height in lines (default: 1) |
## Colors
### Named Colors
Basic 16-color palette supported by all terminals:
| Color | Keyword | Bright Version |
|-------|---------|----------------|
| Black | `:black` | `:bright-black` |
| Red | `:red` | `:bright-red` |
| Green | `:green` | `:bright-green` |
| Yellow | `:yellow` | `:bright-yellow` |
| Blue | `:blue` | `:bright-blue` |
| Magenta | `:magenta` | `:bright-magenta` |
| Cyan | `:cyan` | `:bright-cyan` |
| White | `:white` | `:bright-white` |
| Default | `:default` | - |
Aliases: `:gray` and `:grey` map to `:bright-black`
```clojure
[:text {:fg :red} "Error"]
[:text {:fg :bright-green} "Success"]
[:text {:bg :blue :fg :white} "Highlighted"]
```
### 256 Colors
Use integers 0-255 for extended color support:
```clojure
[:text {:fg 208} "Orange (256-color)"]
[:text {:bg 236} "Dark gray background"]
```
**Color ranges:**
- 0-7: Standard colors
- 8-15: Bright colors
- 16-231: 6x6x6 color cube
- 232-255: Grayscale (dark to light)
### True Color (24-bit)
For true color, use the `tui.ansi` namespace directly:
```clojure
(require '[tui.ansi :as ansi])
[:text (ansi/fg-rgb 255 128 0 "Orange text")]
[:text (ansi/bg-rgb 30 30 30 "Dark background")]
```
## Text Styles
Combine multiple styles:
```clojure
[:text {:bold true :underline true} "Bold and underlined"]
[:text {:fg :red :bold true :inverse true} "Inverted error"]
[:text {:dim true :italic true} "Subtle italic"]
```
**Available styles:**
| Style | Attribute | Description |
|-------|-----------|-------------|
| Bold | `:bold true` | Heavier font weight |
| Dim | `:dim true` | Lighter/faded text |
| Italic | `:italic true` | Slanted text |
| Underline | `:underline true` | Line under text |
| Inverse | `:inverse true` | Swap foreground/background |
| Strikethrough | `:strike true` | Line through text |
## Layout Examples
### Two-Column Layout
```clojure
[:row {:gap 4}
[:col
[:text {:bold true} "Left Column"]
[:text "Item 1"]
[:text "Item 2"]]
[:col
[:text {:bold true} "Right Column"]
[:text "Item A"]
[:text "Item B"]]]
```
**Output:**
```
Left Column Right Column
Item 1 Item A
Item 2 Item B
```
### Nested Boxes
```clojure
[:box {:title "Outer" :padding 1}
[:row {:gap 2}
[:box {:border :single :title "Box A"}
[:text "Content A"]]
[:box {:border :single :title "Box B"}
[:text "Content B"]]]]
```
**Output:**
```
╭─Outer────────────────────────────╮
│ │
│ ┌─Box A─────┐ ┌─Box B─────┐ │
│ │Content A │ │Content B │ │
│ └───────────┘ └───────────┘ │
│ │
╰──────────────────────────────────╯
```
### Status Bar
```clojure
[:col
[:box {:border :rounded :padding [0 1]}
[:text {:bold true} "My Application"]]
[:space {:height 1}]
[:text "Main content here..."]
[:space {:height 1}]
[:row {:gap 2}
[:text {:fg :gray} "Status: Ready"]
[:text {:fg :gray} "|"]
[:text {:fg :gray} "Press q to quit"]]]
```
**Output:**
```
╭──────────────────╮
│ My Application │
╰──────────────────╯
Main content here...
Status: Ready | Press q to quit
```
### Menu with Selection
```clojure
(defn menu-item [label selected?]
[:row
[:text (if selected? "> " " ")]
[:text {:fg (if selected? :cyan :white)
:bold selected?}
label]])
(defn view [{:keys [items cursor]}]
[:col
[:text {:bold true} "Select an option:"]
[:space {:height 1}]
[:col {:gap 0}
(for [[idx item] (map-indexed vector items)]
(menu-item item (= idx cursor)))]])
```
**Output (cursor on second item):**
```
Select an option:
First Option
> Second Option
Third Option
```
### Progress Indicator
```clojure
(defn progress-bar [percent width]
(let [filled (int (* width (/ percent 100)))
empty (- width filled)]
[:row
[:text "["]
[:text {:fg :green} (apply str (repeat filled "="))]
[:text {:fg :gray} (apply str (repeat empty "-"))]
[:text "]"]
[:text " "]
[:text (str percent "%")]]))
(defn view [{:keys [progress]}]
[:col
[:text "Downloading..."]
(progress-bar progress 20)])
```
**Output (at 65%):**
```
Downloading...
[=============-------] 65%
```
## Helper Functions
The `tui.render` namespace provides helper functions:
```clojure
(require '[tui.render :refer [text row col box]])
;; These are equivalent:
[:text {:fg :red} "Error"]
(text {:fg :red} "Error")
[:row {:gap 2} "A" "B"]
(row {:gap 2} "A" "B")
[:col [:text "Line 1"] [:text "Line 2"]]
(col (text "Line 1") (text "Line 2"))
[:box {:title "Info"} "Content"]
(box {:title "Info"} "Content")
```
## Conditional Rendering
Use standard Clojure conditionals:
```clojure
(defn view [{:keys [loading? error data]}]
[:col
[:text {:bold true} "Status"]
(cond
loading?
[:text {:fg :yellow} "Loading..."]
error
[:text {:fg :red} (str "Error: " error)]
:else
[:text {:fg :green} (str "Data: " data)])])
```
## Dynamic Views with `for`
Generate repeated elements:
```clojure
(defn view [{:keys [items selected]}]
[:col
(for [[idx item] (map-indexed vector items)]
[:row
[:text (if (= idx selected) "> " " ")]
[:text {:fg (if (= idx selected) :cyan :white)} item]])])
```
## String Shortcuts
Plain strings are automatically wrapped in `:text`:
```clojure
;; These are equivalent:
[:col "Line 1" "Line 2"]
[:col [:text "Line 1"] [:text "Line 2"]]
;; In rows too:
[:row "A" "B" "C"]
[:row [:text "A"] [:text "B"] [:text "C"]]
```
## Common Patterns
### Styled Labels
```clojure
(defn label [text]
[:text {:fg :gray} (str text ": ")])
(defn value [text]
[:text {:fg :white :bold true} text])
[:row (label "Name") (value "John")]
```
### Conditional Styling
```clojure
(defn status-text [status]
[:text {:fg (case status
:ok :green
:warning :yellow
:error :red
:white)
:bold (= status :error)}
(name status)])
```
### Reusable Components
```clojure
(defn card [{:keys [title]} & children]
[:box {:border :rounded :title title :padding [0 1]}
(into [:col] children)])
;; Usage
(card {:title "User Info"}
[:row (label "Name") (value "Alice")]
[:row (label "Email") (value "alice@example.com")])
```
## Next Steps
- [API Reference](api-reference.md) - Complete API documentation
- [Examples](examples.md) - Full example applications