# 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 `after`. **Features:** - Async timer 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/after 1000 :timer-tick))])) ;; Reset with 'r' (tui/key= msg "r") [{:seconds initial-seconds :paused false :done false} (tui/after 1000 :timer-tick)] ;; Handle timer tick (= msg :timer-tick) (if (:paused model) [model nil] (let [new-seconds (dec (:seconds model))] (if (pos? new-seconds) [{:seconds new-seconds :paused false :done false} (tui/after 1000 :timer-tick)] [{: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/after 1000 :timer-tick)})) ``` ### 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. **After command**: `(tui/after 1000 :timer-tick)` sends `:timer-tick` after 1 second 2. **Initial command**: `:init-cmd` starts the first timer 3. **State machine**: Track `paused` and `done` states 4. **Pause control**: Only schedule next `after` 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 timer-based animation. **Features:** - Multiple spinner styles - Fast animation (80ms intervals) - 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/after 80 :spinner-frame)] ;; Handle spinner frame (= msg :spinner-frame) (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/after interval :spinner-frame)])) :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/after 80 :spinner-frame)})) ``` ### 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 timers**: 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 timers 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"])) ```