906 lines
23 KiB
Markdown
906 lines
23 KiB
Markdown
# Examples
|
|
|
|
Detailed walkthroughs of the example applications included with Clojure TUI.
|
|
|
|
## Running Examples
|
|
|
|
```bash
|
|
# With Babashka (recommended - fast startup)
|
|
bb counter
|
|
bb timer
|
|
bb list
|
|
bb spinner
|
|
bb views
|
|
bb http
|
|
|
|
# With full Clojure
|
|
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.core :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.core :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"]))
|
|
```
|