| Hand the window to a REPL, so the UI is something to poke at 757d8e7 nandi 17d ago | 1 | (ns glimmer-vidya.repl |
| 2 | "A window you edit while it is open. |
| 3 | |
| 4 | The UI is one ratom holding hiccup, and a REPL that `reset!`s it. Nothing |
| 5 | here is special to REPLs — it is the ordinary glimmer arrangement, pointed at |
| 6 | a cell whose new value you type rather than one a handler computes. Every |
| 7 | `show!` re-renders through the reconciler, so only what changed is touched: |
| 8 | the window does not blink and an `:entry` keeps its text. |
| 9 | |
| 10 | Two ways in, both of them evaluating in this namespace. |
| 11 | |
| 12 | **A prompt in the terminal** — the loop owns the main thread, a reader thread |
| 13 | owns stdin: |
| 14 | |
| 15 | LD_LIBRARY_PATH=../build jolt repl-ui |
| 16 | |
| 17 | ui=> (show! [:card {} [:title {:label \"live\"}]]) |
| 18 | ui=> (swap! hits inc) |
| 19 | ui=> (dump!) |
| 20 | ui=> :quit |
| 21 | |
| 22 | **An editor, over nREPL** — no terminal prompt; `start!` hands the loop to |
| 23 | the parked main thread, so eval stays free: |
| 24 | |
| 25 | LD_LIBRARY_PATH=../build jolt nrepl-server |
| 26 | |
| 27 | (require '[glimmer-vidya.repl :as live]) |
| 28 | (live/start!) |
| 29 | (live/show! [:label {:label \"from the editor\"}]) |
| 30 | |
| 31 | Both are safe from any thread: `show!` only touches a ratom, and glimmer |
| 32 | marshals the re-render onto the loop thread through the backend's `schedule`. |
| 33 | Code that wants to touch nodes directly goes through `glimmer.core/on-gui`." |
| 34 | (:require [glimmer.ratom :as r :refer [atom]] |
| 35 | [glimmer.core :as ui] |
| 36 | [glimmer-vidya.core :as vidya] |
| 37 | [jolt.host :as host])) |
| 38 | |
| 39 | ;; `defonce`, so re-evaluating this file from the editor keeps the window's |
| 40 | ;; current contents rather than snapping it back to the greeting. |
| 41 | (defonce view |
| 42 | (atom [:page {:max-width 520} |
| 43 | [:card {} |
| 44 | [:title {:label "glimmer-vidya"}] |
| 45 | [:label {:label "Type hiccup at the prompt; this changes."}] |
| 46 | [:dim-label {:label "(show! [:label {:label \"hello\"}])"}]]])) |
| 47 | |
| 48 | ;; A cell of your own to reach for: `(show! (fn [] ... @hits ...))` and then |
| 49 | ;; `(swap! hits inc)` from the prompt, or from a handler you installed with it. |
| 50 | (defonce hits (atom 0)) |
| 51 | |
| 52 | (defn root |
| 53 | "The whole app: whatever `view` holds. A component that derefs one cell is |
| 54 | all it takes for the reconciler to follow along. |
| 55 | |
| 56 | A function in there is mounted as a component rather than returned as data, |
| 57 | which is the difference between a picture and a program: hiccup you typed is |
| 58 | fixed at the moment you typed it, while a function re-runs whenever a cell it |
| 59 | derefs changes." |
| 60 | [] |
| 61 | (let [v @view] |
| 62 | (if (fn? v) [v] v))) |
| 63 | |
| 64 | (defn show! |
| 65 | "Replace the window's contents. |
| 66 | |
| 67 | Give it hiccup for a one-off, or a zero-arg function for something that keeps |
| 68 | reacting after the form that defined it returned: |
| 69 | |
| 70 | (show! [:label {:label (str \"hits \" @hits)}]) ; frozen at that count |
| 71 | (show! (fn [] [:label {:label (str \"hits \" @hits)}])) ; follows it |
| 72 | |
| 73 | Returns what it was given, so the REPL prints the tree you asked for." |
| 74 | [hiccup-or-fn] |
| 75 | (reset! view hiccup-or-fn)) |
| 76 | |
| 77 | (defn gui |
| 78 | "Run `f` on the loop thread and return what it returned, waiting up to |
| 79 | `ms` (1s by default) for it. |
| 80 | |
| 81 | Reading or writing a node is the loop thread's business — the library keeps |
| 82 | its arena in thread-local storage, so the same call from here would find an |
| 83 | empty tree rather than fail. `show!` needs none of this, because a ratom is |
| 84 | not a node; anything in `glimmer-vidya.core` that names one does." |
| 85 | ([f] (gui f 1000)) |
| 86 | ([f ms] |
| 87 | (let [p (promise)] |
| 88 | (ui/on-gui #(deliver p (try {:ok (f)} (catch Exception e {:err e})))) |
| 89 | ;; Polled rather than `(deref p ms ...)`: glimmer.ratom replaces `deref` |
| 90 | ;; with a cell-aware one-arity version, so the timeout arity is not there |
| 91 | ;; to call in a namespace that has required it. |
| 92 | (loop [waited 0] |
| 93 | (cond |
| 94 | (realized? p) (let [{:keys [ok err]} @p] (if err (throw err) ok)) |
| 95 | (>= waited ms) (throw (ex-info "no window is running" {:waited-ms ms})) |
| 96 | :else (do (Thread/sleep 10) (recur (+ waited 10)))))))) |
| 97 | |
| 98 | (defn title! |
| 99 | "Rename the open window. Like everything that touches the window rather than |
| 100 | a ratom, it belongs to the loop thread — called straight from here it would |
| 101 | find no app in its thread-local slot and do nothing at all, quietly." |
| 102 | [t] |
| 103 | (gui #(vidya/set-title! t)) |
| 104 | t) |
| 105 | |
| 106 | (defn dump! |
| 107 | "Print what is actually mounted, after reconciliation — the answer to \"did |
| 108 | that render the way I meant?\". Hops to the loop thread to read it." |
| 109 | [] |
| 110 | (println (gui vidya/dump-str)) |
| 111 | nil) |
| 112 | |
| 113 | (defn dump |
| 114 | "`dump!` as hiccup data, for comparing against what you meant to send." |
| 115 | [] |
| 116 | (read-string (gui vidya/dump-str))) |
| 117 | |
| 118 | ;; --- the editor entry point -------------------------------------------------- |
| 119 | (defonce ^:private window (atom nil)) |
| 120 | |
| 121 | (defn start! |
| 122 | "Open the window and return immediately, so an nREPL session stays free to |
| 123 | evaluate. Options are `ui/run`'s. |
| 124 | |
| 125 | The loop is handed to the **main** thread rather than started here. winit |
| 126 | refuses to create an event loop anywhere else, and an nREPL eval runs on a |
| 127 | worker — so the work is posted to the main thread, which |
| 128 | `jolt nrepl-server` has parked in a pump for exactly this. That also means |
| 129 | this belongs in an nREPL session and not in a `-main`, where the main thread |
| 130 | is yours already and `ui/run` is the plainer thing to call. |
| 131 | |
| 132 | Once per process, and this is winit's rule rather than a shortcut here: an |
| 133 | event loop cannot be recreated, so a window that has been closed stays |
| 134 | closed and `start!` says so instead of opening an invisible one. Editing the |
| 135 | UI needs no restart — that is what `show!` is for — but `stop!` does." |
| 136 | [& opts] |
| 137 | (let [w @window] |
| 138 | (cond |
| 139 | (and w (not (realized? w))) |
| 140 | (do (println "already running") nil) |
| 141 | |
| 142 | w |
| 143 | (throw (ex-info (str "this process has already run its window and winit " |
| 144 | "cannot recreate an event loop — restart the nREPL " |
| 145 | "server to get another") |
| 146 | {:result @w})) |
| 147 | |
| 148 | :else |
| 149 | (do (reset! window |
| 150 | (future (host/call-on-main-thread |
| 151 | #(apply ui/run root |
| 152 | :title "repl" :width 560 :height 420 opts)))) |
| 153 | ;; Long enough to catch an immediate failure — a window that cannot |
| 154 | ;; open should throw here, not sit unnoticed inside a future. |
| 155 | (Thread/sleep 300) |
| 156 | (when (realized? @window) @@window) |
| 157 | nil)))) |
| 158 | |
| 159 | (defn stop! |
| 160 | "Close the window, ending this process's one event loop." |
| 161 | [] |
| 162 | (vidya/quit!) |
| 163 | nil) |
| 164 | |
| 165 | ;; --- the terminal entry point ------------------------------------------------ |
| 166 | (defn- prompt-loop! |
| 167 | "Read forms from stdin and evaluate them in this namespace, printing each |
| 168 | result. Runs off the main thread; the frame loop has that one. |
| 169 | |
| 170 | `:quit`, or EOF (Ctrl-D), stops the window and with it the process. A form |
| 171 | that throws prints its exception and the prompt comes back — the window is |
| 172 | never brought down by a typo." |
| 173 | [] |
| 174 | (let [ns' (the-ns 'glimmer-vidya.repl)] |
| 175 | (loop [] |
| 176 | (print "ui=> ") |
| 177 | (flush) |
| 178 | (let [form (try (read {:eof ::eof} *in*) |
| 179 | (catch Exception e (println "read:" (ex-message e)) nil))] |
| 180 | (cond |
| 181 | (or (= form ::eof) (= form :quit)) (vidya/quit!) |
| 182 | :else (do (when (some? form) |
| 183 | (try (prn (binding [*ns* ns'] (eval form))) |
| 184 | (catch Exception e (println "error:" (ex-message e))))) |
| 185 | (recur))))))) |
| 186 | |
| 187 | (defn -main [& _] |
| 188 | ;; The reader goes on the background thread rather than the loop, because on |
| 189 | ;; macOS the window has to be the main one and because a blocking `read` must |
| 190 | ;; never sit between two frames. |
| 191 | (future (prompt-loop!)) |
| 192 | (ui/run root :title "repl" :width 560 :height 420) |
| 193 | (println "\nrepl: window closed")) |