nandi/jolt-nativepublic Fork 0
8e8cd5192dc161b423c0ee5dd41a7a058b24b409
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

repl.clj · 193 lines · 7.6 KBClojure Blame HistoryRaw
Hand the window to a REPL, so the UI is something to poke at 757d8e7 nandi 17d ago1(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"))