Files
clojure-tui/docs/examples.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

906 lines
23 KiB
Markdown

# 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"]))
```