init
This commit is contained in:
@@ -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))))
|
||||
@@ -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)))))
|
||||
@@ -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))))))
|
||||
@@ -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."))
|
||||
@@ -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!"))))
|
||||
@@ -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."))
|
||||
Reference in New Issue
Block a user