update examples. fix bugs
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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]))))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user