(ns glimmer-vidya.repl "A window you edit while it is open. The UI is one ratom holding hiccup, and a REPL that `reset!`s it. Nothing here is special to REPLs — it is the ordinary glimmer arrangement, pointed at a cell whose new value you type rather than one a handler computes. Every `show!` re-renders through the reconciler, so only what changed is touched: the window does not blink and an `:entry` keeps its text. Two ways in, both of them evaluating in this namespace. **A prompt in the terminal** — the loop owns the main thread, a reader thread owns stdin: LD_LIBRARY_PATH=../build jolt repl-ui ui=> (show! [:card {} [:title {:label \"live\"}]]) ui=> (swap! hits inc) ui=> (dump!) ui=> :quit **An editor, over nREPL** — no terminal prompt; `start!` hands the loop to the parked main thread, so eval stays free: LD_LIBRARY_PATH=../build jolt nrepl-server (require '[glimmer-vidya.repl :as live]) (live/start!) (live/show! [:label {:label \"from the editor\"}]) Both are safe from any thread: `show!` only touches a ratom, and glimmer marshals the re-render onto the loop thread through the backend's `schedule`. Code that wants to touch nodes directly goes through `glimmer.core/on-gui`." (:require [glimmer.ratom :as r :refer [atom]] [glimmer.core :as ui] [glimmer-vidya.core :as vidya] [jolt.host :as host])) ;; `defonce`, so re-evaluating this file from the editor keeps the window's ;; current contents rather than snapping it back to the greeting. (defonce view (atom [:page {:max-width 520} [:card {} [:title {:label "glimmer-vidya"}] [:label {:label "Type hiccup at the prompt; this changes."}] [:dim-label {:label "(show! [:label {:label \"hello\"}])"}]]])) ;; A cell of your own to reach for: `(show! (fn [] ... @hits ...))` and then ;; `(swap! hits inc)` from the prompt, or from a handler you installed with it. (defonce hits (atom 0)) (defn root "The whole app: whatever `view` holds. A component that derefs one cell is all it takes for the reconciler to follow along. A function in there is mounted as a component rather than returned as data, which is the difference between a picture and a program: hiccup you typed is fixed at the moment you typed it, while a function re-runs whenever a cell it derefs changes." [] (let [v @view] (if (fn? v) [v] v))) (defn show! "Replace the window's contents. Give it hiccup for a one-off, or a zero-arg function for something that keeps reacting after the form that defined it returned: (show! [:label {:label (str \"hits \" @hits)}]) ; frozen at that count (show! (fn [] [:label {:label (str \"hits \" @hits)}])) ; follows it Returns what it was given, so the REPL prints the tree you asked for." [hiccup-or-fn] (reset! view hiccup-or-fn)) (defn gui "Run `f` on the loop thread and return what it returned, waiting up to `ms` (1s by default) for it. Reading or writing a node is the loop thread's business — the library keeps its arena in thread-local storage, so the same call from here would find an empty tree rather than fail. `show!` needs none of this, because a ratom is not a node; anything in `glimmer-vidya.core` that names one does." ([f] (gui f 1000)) ([f ms] (let [p (promise)] (ui/on-gui #(deliver p (try {:ok (f)} (catch Exception e {:err e})))) ;; Polled rather than `(deref p ms ...)`: glimmer.ratom replaces `deref` ;; with a cell-aware one-arity version, so the timeout arity is not there ;; to call in a namespace that has required it. (loop [waited 0] (cond (realized? p) (let [{:keys [ok err]} @p] (if err (throw err) ok)) (>= waited ms) (throw (ex-info "no window is running" {:waited-ms ms})) :else (do (Thread/sleep 10) (recur (+ waited 10)))))))) (defn title! "Rename the open window. Like everything that touches the window rather than a ratom, it belongs to the loop thread — called straight from here it would find no app in its thread-local slot and do nothing at all, quietly." [t] (gui #(vidya/set-title! t)) t) (defn dump! "Print what is actually mounted, after reconciliation — the answer to \"did that render the way I meant?\". Hops to the loop thread to read it." [] (println (gui vidya/dump-str)) nil) (defn dump "`dump!` as hiccup data, for comparing against what you meant to send." [] (read-string (gui vidya/dump-str))) ;; --- the editor entry point -------------------------------------------------- (defonce ^:private window (atom nil)) (defn start! "Open the window and return immediately, so an nREPL session stays free to evaluate. Options are `ui/run`'s. The loop is handed to the **main** thread rather than started here. winit refuses to create an event loop anywhere else, and an nREPL eval runs on a worker — so the work is posted to the main thread, which `jolt nrepl-server` has parked in a pump for exactly this. That also means this belongs in an nREPL session and not in a `-main`, where the main thread is yours already and `ui/run` is the plainer thing to call. Once per process, and this is winit's rule rather than a shortcut here: an event loop cannot be recreated, so a window that has been closed stays closed and `start!` says so instead of opening an invisible one. Editing the UI needs no restart — that is what `show!` is for — but `stop!` does." [& opts] (let [w @window] (cond (and w (not (realized? w))) (do (println "already running") nil) w (throw (ex-info (str "this process has already run its window and winit " "cannot recreate an event loop — restart the nREPL " "server to get another") {:result @w})) :else (do (reset! window (future (host/call-on-main-thread #(apply ui/run root :title "repl" :width 560 :height 420 opts)))) ;; Long enough to catch an immediate failure — a window that cannot ;; open should throw here, not sit unnoticed inside a future. (Thread/sleep 300) (when (realized? @window) @@window) nil)))) (defn stop! "Close the window, ending this process's one event loop." [] (vidya/quit!) nil) ;; --- the terminal entry point ------------------------------------------------ (defn- prompt-loop! "Read forms from stdin and evaluate them in this namespace, printing each result. Runs off the main thread; the frame loop has that one. `:quit`, or EOF (Ctrl-D), stops the window and with it the process. A form that throws prints its exception and the prompt comes back — the window is never brought down by a typo." [] (let [ns' (the-ns 'glimmer-vidya.repl)] (loop [] (print "ui=> ") (flush) (let [form (try (read {:eof ::eof} *in*) (catch Exception e (println "read:" (ex-message e)) nil))] (cond (or (= form ::eof) (= form :quit)) (vidya/quit!) :else (do (when (some? form) (try (prn (binding [*ns* ns'] (eval form))) (catch Exception e (println "error:" (ex-message e))))) (recur))))))) (defn -main [& _] ;; The reader goes on the background thread rather than the loop, because on ;; macOS the window has to be the main one and because a blocking `read` must ;; never sit between two frames. (future (prompt-loop!)) (ui/run root :title "repl" :width 560 :height 420) (println "\nrepl: window closed"))