update examples. fix bugs

This commit is contained in:
2026-02-03 12:53:52 -05:00
parent 9150c90ad1
commit 426a0c4715
15 changed files with 867 additions and 1148 deletions
+10
View File
@@ -236,3 +236,13 @@
true
active-styles))))))))]
(apply str result)))))
(defn fit-width
"Fit string to exactly the given width - truncate if too long, pad if too short.
Unlike truncate, this does not add ellipsis - it hard clips the content."
[s width]
(let [vlen (visible-length s)]
(cond
(= vlen width) s
(> vlen width) (visible-subs s 0 width)
:else (str s (apply str (repeat (- width vlen) " "))))))
+31 -168
View File
@@ -1,68 +1,29 @@
(ns tui.core
"Core TUI framework - Elm architecture runtime.
## New API (Recommended)
Update function signature:
(fn [{:keys [model event]}]
{:model new-model
:events [...]}) ; :events is optional
Events are maps with :type discriminator. See tui.events namespace.
## Legacy API (Deprecated)
For backward compatibility, the old signature is still supported:
(fn [model msg] [new-model cmd])
The runtime auto-detects which format is being used."
Events are maps with :type discriminator. See tui.events namespace."
(:require [tui.terminal :as term]
[tui.input :as input]
[tui.render :as render]
[tui.ansi :as ansi]
[tui.events :as ev]
[clojure.core.async :as async :refer [go go-loop chan <! >! >!! <!! put! close! timeout alt! alt!!]]))
;; === Legacy Command Types (Deprecated) ===
;; nil - no-op
;; [:quit] - exit program
;; [:batch cmd1 cmd2 ...] - run commands in parallel
;; [:seq cmd1 cmd2 ...] - run commands sequentially
;; (fn [] msg) - arbitrary async function returning message
;; Re-export commonly used functions from tui.events for convenience
(def quit ev/quit)
(def delayed-event ev/delayed-event)
(def batch ev/batch)
(def sequential ev/sequential)
(def shell ev/shell)
(def debounce ev/debounce)
(def key= ev/key=)
;; === Legacy Built-in Commands (Deprecated) ===
(def quit
"DEPRECATED: Use (tui.events/quit) instead."
[:quit])
(defn after
"DEPRECATED: Use (tui.events/delay ms event) instead.
Returns a command that sends msg after ms milliseconds."
[ms msg]
(fn []
(Thread/sleep ms)
msg))
(defn batch
"DEPRECATED: Use (tui.events/batch ...) instead.
Run multiple commands in parallel."
[& cmds]
(into [:batch] (remove nil? cmds)))
(defn sequentially
"DEPRECATED: Use (tui.events/sequential ...) instead.
Run multiple commands sequentially."
[& cmds]
(into [:seq] (remove nil? cmds)))
(defn send-msg
"DEPRECATED: Put event directly in :events vector instead.
Create a command that sends a message."
[msg]
(fn [] msg))
;; Re-export render function
(def render render/render)
;; === Debounce State ===
(def ^:private debounce-timers (atom {}))
@@ -70,7 +31,7 @@
;; === Event Execution ===
(defn- execute-event!
"Execute an event, putting resulting events on the channel.
Handles runtime events (:quit, :delay, :shell, :batch, :sequential, :debounce).
Handles runtime events (:quit, :delayed-event, :shell, :batch, :sequential, :debounce).
Unknown event types are dispatched back to the update function."
[event msg-chan]
(when event
@@ -80,8 +41,8 @@
:quit
(put! msg-chan {:type :quit})
;; Delay - wait then dispatch event
:delay
;; Delayed event - wait then dispatch event
:delayed-event
(let [{:keys [ms event]} event]
(go
(<! (timeout ms))
@@ -148,41 +109,6 @@
;; Unknown type - dispatch to update function
(put! msg-chan event)))))
;; === Legacy Command Execution ===
(defn- execute-cmd!
"Execute a legacy command, putting resulting messages on the channel."
[cmd msg-chan]
(when cmd
(cond
;; Quit command
(= cmd [:quit])
(put! msg-chan {:type :quit})
;; Batch - run all in parallel
(and (vector? cmd) (= (first cmd) :batch))
(doseq [c (rest cmd)]
(execute-cmd! c msg-chan))
;; Sequence - run one after another
(and (vector? cmd) (= (first cmd) :seq))
(go-loop [[c & rest-cmds] (rest cmd)]
(when c
(let [result-chan (chan 1)]
(execute-cmd! c result-chan)
(when-let [msg (<! result-chan)]
(>! msg-chan msg)
(recur rest-cmds)))))
;; Function - execute and send result
(fn? cmd)
(go
(let [msg (cmd)]
(when msg
(>! msg-chan msg))))
:else
nil)))
;; === Input Loop ===
(defn- start-input-loop!
"Start thread that reads input and puts events on channel.
@@ -198,61 +124,24 @@
(Thread/sleep 10))
(recur)))))
;; === Update Function Detection ===
(defn- detect-update-format
"Detect if update function uses new or legacy format by examining its signature.
Returns :new or :legacy."
[update-fn]
;; We can't easily detect at compile time, so we'll detect at runtime
;; by checking the result format
:unknown)
(defn- call-update
"Call update function, handling both new and legacy formats.
Returns {:model m :events [...]} in new format."
[update-fn model event legacy-mode?]
(if legacy-mode?
;; Legacy: (fn [model msg] [new-model cmd])
(let [[new-model cmd] (update-fn model event)]
{:model new-model
:legacy-cmd cmd})
;; New: (fn [{:keys [model event]}] {:model m :events [...]})
(let [result (update-fn {:model model :event event})]
(if (vector? result)
;; Got legacy format back, switch to legacy mode
{:model (first result)
:legacy-cmd (second result)
:switch-to-legacy true}
;; New format
result))))
;; === Main Run Loop ===
(defn run
"Run a TUI application.
## New API (Recommended)
Options:
- :init - Initial model (required)
- :update - (fn [{:keys [model event]}] {:model m :events [...]}) (required)
- :view - (fn [model size] hiccup) where size is {:width w :height h} (required)
- :fps - Target frames per second (default 60)
- :alt-screen - Use alternate screen buffer (default true)
## Legacy API (Deprecated)
Also accepts:
- :update - (fn [model msg] [new-model cmd])
- :init-cmd - Initial command to run
- :init - Initial model (required)
- :update - (fn [{:keys [model event]}] {:model m :events [...]}) (required)
- :view - (fn [model size] hiccup) where size is {:width w :height h} (required)
- :init-events - Vector of events to dispatch at startup
- :fps - Target frames per second (default 60)
- :alt-screen - Use alternate screen buffer (default true)
Returns the final model."
[{:keys [init update view init-cmd init-events fps alt-screen]
[{:keys [init update view init-events fps alt-screen]
:or {fps 60 alt-screen true}}]
(let [msg-chan (chan 256)
running? (atom true)
frame-time (/ 1000 fps)
;; Start in auto-detect mode, will switch to legacy if needed
legacy-mode? (atom false)]
frame-time (/ 1000 fps)]
;; Setup terminal
(term/raw-mode!)
@@ -264,12 +153,10 @@
;; Start input loop
(start-input-loop! msg-chan running?)
;; Execute initial events/command
;; Execute initial events
(when init-events
(doseq [event init-events]
(execute-event! event msg-chan)))
(when init-cmd
(execute-cmd! init-cmd msg-chan))
;; Initial render
(let [size (term/get-terminal-size)
@@ -291,27 +178,22 @@
(recur model (System/currentTimeMillis))
;; Check for quit
(if (or (= event {:type :quit})
(= event [:quit])) ; legacy
(if (= (:type event) :quit)
;; Quit - return final model
model
;; Update model
(let [result (call-update update model event @legacy-mode?)
_ (when (:switch-to-legacy result)
(reset! legacy-mode? true))
(let [result (update {:model model :event event})
new-model (:model result)
size (term/get-terminal-size)
ctx {:available-height (:height size)
:available-width (:width size)}
now (System/currentTimeMillis)]
;; Execute events (new API) or command (legacy)
(if-let [events (:events result)]
;; Execute events
(when-let [events (:events result)]
(doseq [e events]
(execute-event! e msg-chan))
(when-let [cmd (:legacy-cmd result)]
(execute-cmd! cmd msg-chan)))
(execute-event! e msg-chan)))
;; Render with context for flex layouts
(term/render! (render/render (view new-model size) ctx))
@@ -335,30 +217,11 @@
(defapp my-app
:init {:count 0}
:update (fn [ctx] ...)
:update (fn [{:keys [model event]}] ...)
:view (fn [model size] ...))"
[name & {:keys [init update view init-cmd init-events]}]
[name & {:keys [init update view init-events]}]
`(def ~name
{:init ~init
:update ~update
:view ~view
:init-cmd ~init-cmd
:init-events ~init-events}))
;; === Legacy Key Matching Helpers (Deprecated) ===
(defn key=
"DEPRECATED: Use tui.events/key= instead.
Check if message is a specific key."
[msg key-pattern]
(input/key-match? msg key-pattern))
(defn key-str
"DEPRECATED: Use tui.input/key->str instead.
Get string representation of key."
[msg]
(input/key->str msg))
;; Re-export render function
(def render render/render)
+13 -10
View File
@@ -15,7 +15,7 @@
{:type :key, :key :enter} ; special key
{:type :key, :key \\c, :modifiers #{:ctrl}} ; with modifiers
{:type :quit} ; quit app
{:type :delay, :ms 2000, :event {...}} ; delayed event
{:type :delayed-event, :ms 2000, :event {...}} ; delayed event
## Update Function Contract
@@ -126,18 +126,21 @@
[]
{:type :quit})
(defn delay
(defn delayed-event
"Create an event that dispatches another event after a delay.
The nested event is dispatched after the specified milliseconds elapse.
Useful for transient messages, animations, debouncing, or timeouts.
Like JavaScript's setTimeout - schedules an event to be dispatched
after the specified milliseconds elapse. Useful for transient messages,
animations, debouncing, or timeouts.
Note: Named delayed-event to avoid conflict with clojure.core/delay.
Arguments:
ms - Delay in milliseconds before dispatching
event - Event map to dispatch after the delay
Returns:
{:type :delay, :ms <ms>, :event <event>}
{:type :delayed-event, :ms <ms>, :event <event>}
Examples:
;; Show a message that auto-clears after 3 seconds
@@ -145,7 +148,7 @@
(case (:type event)
:show-message
{:model (assoc model :message (:text event))
:events [(delay 3000 {:type :clear-message})]}
:events [(delayed-event 3000 {:type :clear-message})]}
:clear-message
{:model (dissoc model :message)}
@@ -158,13 +161,13 @@
{:model (-> model
(update :buffer conj event)
(assoc :dirty true))
:events [(delay 5000 {:type :auto-save})]}
:events [(delayed-event 5000 {:type :auto-save})]}
{:model model}))
;; Simple animation frame
{:events [(delay 16 {:type :animation-tick})]}"
{:events [(delayed-event 16 {:type :animation-tick})]}"
[ms event]
{:type :delay, :ms ms, :event event})
{:type :delayed-event, :ms ms, :event event})
(defn shell
"Create an event that runs a shell command asynchronously.
@@ -274,7 +277,7 @@
;; Show message, wait, then clear
(sequential
{:type :show-message, :text \"Saved!\"}
(delay 2000 {:type :clear-message}))
(delayed-event 2000 {:type :clear-message}))
Note:
For complex workflows, consider handling each step explicitly
+3 -3
View File
@@ -286,10 +286,10 @@
box-width (or target-width (+ (max inner-width title-width) 2))
content-width (- box-width 2)
;; Pad lines
;; Pad lines (and truncate if too long)
padded-lines (for [line lines]
(str (apply str (repeat pad-left " "))
(ansi/pad-right line (- content-width pad-left pad-right))
(ansi/fit-width line (- content-width pad-left pad-right))
(apply str (repeat pad-right " "))))
;; Add vertical padding
@@ -322,7 +322,7 @@
(:br chars))
body-lines (for [line all-lines]
(str (:v chars)
(ansi/pad-right line content-width)
(ansi/fit-width line content-width)
(:v chars)))]
(str/join "\n" (concat [top-line] body-lines [bottom-line]))))