This commit is contained in:
2026-01-21 01:16:37 -05:00
commit a990076b03
17 changed files with 2713 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
(ns examples.counter
"Simple counter example - demonstrates basic Elm architecture.
Mirrors bubbletea's simple example."
(:require [tui.core :as tui]))
;; === Model ===
(def initial-model
{:count 0})
;; === Update ===
(defn update-model [model msg]
(cond
;; Quit on q or ctrl+c
(or (tui/key= msg "q")
(tui/key= msg [:ctrl \c]))
[model tui/quit]
;; Increment on up/k
(or (tui/key= msg :up)
(tui/key= msg "k"))
[(update model :count inc) nil]
;; Decrement on down/j
(or (tui/key= msg :down)
(tui/key= msg "j"))
[(update model :count dec) nil]
;; Reset on r
(tui/key= msg "r")
[(assoc model :count 0) nil]
:else
[model nil]))
;; === View ===
(defn view [{:keys [count]}]
[:col {:gap 1}
[:box {:border :rounded :padding [0 1]}
[:col
[:text {:bold true} "Counter"]
[:text ""]
[:text {:fg (cond
(pos? count) :green
(neg? count) :red
:else :default)}
(str "Count: " count)]]]
[:text {:fg :gray} "j/k or up/down: change value"]
[:text {:fg :gray} "r: reset q: quit"]])
;; === Main ===
(defn -main [& _args]
(println "Starting counter...")
(let [final-model (tui/run {:init initial-model
:update update-model
:view view})]
(println "Final count:" (:count final-model))))
+111
View File
@@ -0,0 +1,111 @@
(ns examples.http
"HTTP request example - demonstrates async commands.
Mirrors bubbletea's http example."
(:require [tui.core :as tui]
[clojure.java.io :as io])
(:import [java.net URL HttpURLConnection]))
;; === HTTP Helpers ===
(defn http-get
"Simple HTTP GET request. Returns {:status code :body string} or {:error msg}"
[url-str]
(try
(let [url (URL. url-str)
conn ^HttpURLConnection (.openConnection url)]
(.setRequestMethod conn "GET")
(.setConnectTimeout conn 5000)
(.setReadTimeout conn 5000)
(let [status (.getResponseCode conn)
body (with-open [r (io/reader (.getInputStream conn))]
(slurp r))]
{:status status :body body}))
(catch Exception e
{:error (.getMessage e)})))
;; === Model ===
(def initial-model
{:state :idle ; :idle, :loading, :success, :error
:status nil
:error nil
:url "https://httpstat.us/200"})
;; === Commands ===
(defn fetch-url [url]
(fn []
(let [result (http-get url)]
(if (:error result)
[:http-error (:error result)]
[:http-success (:status result)]))))
;; === Update ===
(defn update-model [{:keys [url] :as model} msg]
(cond
;; Quit
(or (tui/key= msg "q")
(tui/key= msg [:ctrl \c]))
[model tui/quit]
;; Enter - start request
(and (= (:state model) :idle)
(tui/key= msg :enter))
[(assoc model :state :loading) (fetch-url url)]
;; r - retry/reset
(tui/key= msg "r")
[(assoc model :state :idle :status nil :error nil) nil]
;; HTTP success
(= (first msg) :http-success)
[(assoc model :state :success :status (second msg)) nil]
;; HTTP error
(= (first msg) :http-error)
[(assoc model :state :error :error (second msg)) nil]
:else
[model nil]))
;; === View ===
(defn view [{:keys [state status error url]}]
[:col {:gap 1}
[:box {:border :rounded :padding [1 2]}
[:col {:gap 1}
[:text {:bold true} "HTTP Request Demo"]
[:text ""]
[:row {:gap 1}
[:text {:fg :gray} "URL:"]
[:text {:fg :cyan} url]]
[:text ""]
(case state
:idle
[:text {:fg :gray} "Press enter to fetch..."]
:loading
[:row {:gap 1}
[:text {:fg :yellow} "⠋"]
[:text "Fetching..."]]
:success
[:row {:gap 1}
[:text {:fg :green} "✓"]
[:text (str "Status: " status)]]
:error
[:col
[:row {:gap 1}
[:text {:fg :red} "✗"]
[:text {:fg :red} "Error:"]]
[:text {:fg :red} error]])]]
[:text {:fg :gray}
(if (= state :idle)
"enter: fetch q: quit"
"r: retry q: quit")]])
;; === Main ===
(defn -main [& _args]
(println "Starting HTTP demo...")
(let [final (tui/run {:init initial-model
:update update-model
:view view})]
(when (= (:state final) :success)
(println "Request completed with status:" (:status final)))))
+92
View File
@@ -0,0 +1,92 @@
(ns examples.list-selection
"List selection example - demonstrates cursor navigation and multi-select.
Mirrors bubbletea's list examples."
(:require [tui.core :as tui]
[clojure.string :as str]))
;; === Model ===
(def initial-model
{:cursor 0
:items ["Pizza" "Sushi" "Tacos" "Burger" "Pasta" "Salad" "Soup" "Steak"]
:selected #{}
:submitted false})
;; === Update ===
(defn update-model [{:keys [cursor items] :as model} msg]
(cond
;; Quit
(or (tui/key= msg "q")
(tui/key= msg [:ctrl \c]))
[model tui/quit]
;; Move up
(or (tui/key= msg :up)
(tui/key= msg "k"))
[(update model :cursor #(max 0 (dec %))) nil]
;; Move down
(or (tui/key= msg :down)
(tui/key= msg "j"))
[(update model :cursor #(min (dec (count items)) (inc %))) nil]
;; Toggle selection
(tui/key= msg " ")
[(update model :selected
#(if (contains? % cursor)
(disj % cursor)
(conj % cursor)))
nil]
;; Submit
(tui/key= msg :enter)
[(assoc model :submitted true) tui/quit]
:else
[model nil]))
;; === View ===
(defn view [{:keys [cursor items selected submitted]}]
(if submitted
[:col
[:text {:bold true :fg :green} "You selected:"]
[:text ""]
(if (empty? selected)
[:text {:fg :gray} "(nothing selected)"]
[:col
(for [idx (sort selected)]
[:text (str " - " (nth items idx))])])]
[:col {:gap 1}
[:box {:border :rounded :padding [0 1] :title "What's for lunch?"}
[:col
(for [[idx item] (map-indexed vector items)]
(let [is-cursor (= idx cursor)
is-selected (contains? selected idx)]
[:row {:gap 1}
[:text {:fg (when is-cursor :cyan)} (if is-cursor ">" " ")]
[:text (if is-selected "[x]" "[ ]")]
[:text {:bold is-cursor
:fg (cond
is-selected :green
is-cursor :cyan
:else :default)}
item]]))]]
[:row {:gap 2}
[:text {:fg :gray} "j/k: move"]
[:text {:fg :gray} "space: select"]
[:text {:fg :gray} "enter: confirm"]
[:text {:fg :gray} "q: quit"]]
[:text {:fg :cyan}
(str (count selected) " item" (when (not= 1 (count selected)) "s") " selected")]]))
;; === Main ===
(defn -main [& _args]
(println "Starting list selection...")
(let [{:keys [items selected submitted]} (tui/run {:init initial-model
:update update-model
:view view})]
(when submitted
(println)
(println "Your order:")
(doseq [idx (sort selected)]
(println " -" (nth items idx))))))
+90
View File
@@ -0,0 +1,90 @@
(ns examples.spinner
"Spinner example - demonstrates animated loading states.
Mirrors bubbletea's spinner example."
(:require [tui.core :as tui]))
;; === Spinner Frames ===
(def spinner-styles
{:dots ["⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏"]
:line ["|" "/" "-" "\\"]
:circle ["◐" "◓" "◑" "◒"]
:square ["◰" "◳" "◲" "◱"]
:triangle ["◢" "◣" "◤" "◥"]
:bounce ["⠁" "⠂" "⠄" "⠂"]
:dots2 ["⣾" "⣽" "⣻" "⢿" "⡿" "⣟" "⣯" "⣷"]
:arc ["◜" "◠" "◝" "◞" "◡" "◟"]
:moon ["🌑" "🌒" "🌓" "🌔" "🌕" "🌖" "🌗" "🌘"]})
;; === Model ===
(def initial-model
{:frame 0
:style :dots
:loading true
:message "Loading..."
:styles (keys spinner-styles)
:style-idx 0})
;; === Update ===
(defn update-model [{:keys [styles style-idx] :as model} msg]
(cond
;; Quit
(or (tui/key= msg "q")
(tui/key= msg [:ctrl \c]))
[model tui/quit]
;; Tick - advance frame
(= (first msg) :tick)
(if (:loading model)
[(update model :frame inc) (tui/tick 80)]
[model nil])
;; Space - simulate completion
(tui/key= msg " ")
[(assoc model :loading false :message "Done!") nil]
;; Tab - change spinner style
(tui/key= msg :tab)
(let [new-idx (mod (inc style-idx) (count styles))]
[(assoc model
:style-idx new-idx
:style (nth styles new-idx))
nil])
;; r - restart
(tui/key= msg "r")
[(assoc model :loading true :frame 0 :message "Loading...")
(tui/tick 80)]
:else
[model nil]))
;; === View ===
(defn spinner-view [{:keys [frame style]}]
(let [frames (get spinner-styles style)
idx (mod frame (count frames))]
(nth frames idx)))
(defn view [{:keys [loading message style] :as model}]
[:col {:gap 1}
[:box {:border :rounded :padding [1 2]}
[:col {:gap 1}
[:text {:bold true} "Spinner Demo"]
[:text ""]
[:row {:gap 1}
(if loading
[:text {:fg :cyan} (spinner-view model)]
[:text {:fg :green} "✓"])
[:text message]]
[:text ""]
[:text {:fg :gray} (str "Style: " (name style))]]]
[:col
[:text {:fg :gray} "tab: change style space: complete r: restart q: quit"]]])
;; === Main ===
(defn -main [& _args]
(println "Starting spinner...")
(tui/run {:init initial-model
:update update-model
:view view
:init-cmd (tui/tick 80)})
(println "Spinner demo finished."))
+81
View File
@@ -0,0 +1,81 @@
(ns examples.timer
"Countdown timer example - demonstrates async commands.
Mirrors bubbletea's stopwatch/timer examples."
(:require [tui.core :as tui]))
;; === Model ===
(def initial-model
{:seconds 10
:running true
:done false})
;; === Update ===
(defn update-model [model msg]
(cond
;; Quit
(or (tui/key= msg "q")
(tui/key= msg [:ctrl \c]))
[model tui/quit]
;; Tick - decrement timer
(= (first msg) :tick)
(if (:running model)
(let [new-seconds (dec (:seconds model))]
(if (<= new-seconds 0)
;; Timer done
[(assoc model :seconds 0 :done true :running false) nil]
;; Continue countdown
[(assoc model :seconds new-seconds) (tui/tick 1000)]))
[model nil])
;; Space - pause/resume
(tui/key= msg " ")
(let [new-running (not (:running model))]
[(assoc model :running new-running)
(when new-running (tui/tick 1000))])
;; r - reset
(tui/key= msg "r")
[(assoc model :seconds 10 :done false :running true)
(tui/tick 1000)]
:else
[model nil]))
;; === View ===
(defn format-time [seconds]
(let [mins (quot seconds 60)
secs (mod seconds 60)]
(format "%02d:%02d" mins secs)))
(defn view [{:keys [seconds running done]}]
[:col {:gap 1}
[:box {:border :rounded :padding [1 2]}
[:col
[:text {:bold true} "Countdown Timer"]
[:text ""]
[:text {:fg (cond
done :green
(< seconds 5) :red
:else :cyan)
:bold true}
(if done
"Time's up!"
(format-time seconds))]
[:text ""]
[:text {:fg :gray}
(cond
done "Press r to restart"
running "Running..."
:else "Paused")]]]
[:text {:fg :gray} "space: pause/resume r: reset q: quit"]])
;; === Main ===
(defn -main [& _args]
(println "Starting timer...")
(let [final-model (tui/run {:init initial-model
:update update-model
:view view
:init-cmd (tui/tick 1000)})]
(when (:done final-model)
(println "Timer completed!"))))
+122
View File
@@ -0,0 +1,122 @@
(ns examples.views
"Multiple views example - demonstrates state machine pattern.
Mirrors bubbletea's views example."
(:require [tui.core :as tui]))
;; === Model ===
(def initial-model
{:view :menu ; :menu, :detail, :confirm
:cursor 0
:items [{:name "Profile" :desc "View and edit your profile settings"}
{:name "Settings" :desc "Configure application preferences"}
{:name "Help" :desc "Get help and documentation"}
{:name "About" :desc "About this application"}]
:selected nil})
;; === Update ===
(defn update-model [{:keys [view cursor items] :as model} msg]
(case view
;; Menu view
:menu
(cond
(or (tui/key= msg "q")
(tui/key= msg [:ctrl \c]))
[model tui/quit]
(or (tui/key= msg :up)
(tui/key= msg "k"))
[(update model :cursor #(max 0 (dec %))) nil]
(or (tui/key= msg :down)
(tui/key= msg "j"))
[(update model :cursor #(min (dec (count items)) (inc %))) nil]
(tui/key= msg :enter)
[(assoc model
:view :detail
:selected (nth items cursor))
nil]
:else
[model nil])
;; Detail view
:detail
(cond
(or (tui/key= msg "q")
(tui/key= msg [:ctrl \c]))
[(assoc model :view :confirm) nil]
(or (tui/key= msg :escape)
(tui/key= msg "b"))
[(assoc model :view :menu :selected nil) nil]
:else
[model nil])
;; Confirm quit dialog
:confirm
(cond
(tui/key= msg "y")
[model tui/quit]
(or (tui/key= msg "n")
(tui/key= msg :escape))
[(assoc model :view :detail) nil]
:else
[model nil])))
;; === Views ===
(defn menu-view [{:keys [cursor items]}]
[:col {:gap 1}
[:box {:border :rounded :padding [0 1] :title "Main Menu"}
[:col
(for [[idx item] (map-indexed vector items)]
(let [is-cursor (= idx cursor)]
[:row {:gap 1}
[:text {:fg (when is-cursor :cyan)} (if is-cursor ">" " ")]
[:text {:bold is-cursor
:fg (when is-cursor :cyan)}
(:name item)]]))]]
[:text {:fg :gray} "j/k: navigate enter: select q: quit"]])
(defn detail-view [{:keys [selected]}]
[:col {:gap 1}
[:box {:border :double :padding [1 2]}
[:col {:gap 1}
[:text {:bold true :fg :cyan} (:name selected)]
[:text ""]
[:text (:desc selected)]
[:text ""]
[:text {:fg :gray :italic true}
"This is a detailed view of the selected item."]
[:text {:fg :gray :italic true}
"You could show forms, settings, or other content here."]]]
[:text {:fg :gray} "b/esc: back q: quit"]])
(defn confirm-view [_model]
[:col {:gap 1}
[:box {:border :rounded :padding [1 2]}
[:col
[:text {:bold true :fg :yellow} "Quit?"]
[:text ""]
[:text "Are you sure you want to quit?"]
[:text ""]
[:row {:gap 2}
[:text {:fg :green} "[y] Yes"]
[:text {:fg :red} "[n] No"]]]]])
(defn view [{:keys [view] :as model}]
(case view
:menu (menu-view model)
:detail (detail-view model)
:confirm (confirm-view model)))
;; === Main ===
(defn -main [& _args]
(println "Starting views demo...")
(tui/run {:init initial-model
:update update-model
:view view})
(println "Views demo finished."))