nandi/jolt-nativepublic Fork 0
757d8e7
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.

Hand the window to a REPL, so the UI is something to poke at

glimmer-vidya could be run and it could be tested, but there was no way
to hold a running window still and change it. `jolt counter` recompiles
the component every time you want a different one, which is the loop this
toolkit exists to avoid.

`jolt repl-ui` is that window. Its whole contents are one ratom and a
prompt that writes to it — nothing in the arrangement is special to
REPLs, only the cell is driven by what you type rather than by a handler.
Hiccup is fixed at the moment you type it; a zero-arg function is mounted
as a component and re-runs whenever a cell it derefs changes, so
`(show! (fn [] [:label {:label (str "hits " @hits)}]))` follows a later
`(swap! hits inc)`. Either way the reconciler patches what changed, and
an `:entry` keeps its text and cursor across a `show!`.

Two boundary rules shaped it, both the library's rather than glimmer's.

The frame loop must own the main thread — winit will not create an event
loop anywhere else — so `-main` paints on the main thread and reads stdin
on a second one. From an editor it is the other way around: `jolt
nrepl-server` parks the main thread in a pump, and `start!` posts the
loop to it through `jolt.host/call-on-main-thread`, leaving eval free. An
event loop cannot be recreated either, so `stop!` ends the window for the
life of that process and `start!` says so rather than opening an
invisible one.

Nodes belong to the loop thread. `show!` is safe from anywhere because a
ratom is not a node, but the library keeps its arena in thread-local
storage, so anything that reads or writes the tree has to hop — which is
what `gui` does with `glimmer.core/on-gui`, and why `dump!` returns the
tree rather than an empty root. It waits on a promise by polling, because
`glimmer.ratom` replaces `deref` with a cell-aware one-arity version and
the timeout arity is not there to call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-01T21:55:14-07:00 Browse files
757d8e7 parent: 089e6f7
modified .gitignore +5 -0
@@ -13,3 +13,8 @@
1313
1414 # Staged output of `just buck-build`.
1515 /build/
16+
17+# jolt's dependency-resolution cache, and the port file its nREPL server
18+# writes for an editor to find.
19+**/.jolt/cpcache/
20+**/.nrepl-port
@@ -13,3 +13,8 @@
13 13
14 # Staged output of `just buck-build`.14 # Staged output of `just buck-build`.
15 /build/15 /build/
16+
17+# jolt's dependency-resolution cache, and the port file its nREPL server
18+# writes for an editor to find.
19+**/.jolt/cpcache/
20+**/.nrepl-port
modified jolt/glimmer-vidya/README.md +51 -0
@@ -93,8 +93,59 @@ jolt test # the suite, headless: no window, display or GPU needed
9393 jolt counter # the counter above
9494 jolt showcase # every tag, a keyed task list, an entry, a disabled subtree
9595 jolt smoke # non-interactive: reconciles under paint, then quits
96+jolt repl-ui # a window you edit from a prompt, live
9697 ```
9798
99+## Live from a REPL
100+
101+`examples/glimmer_vidya/repl.jolt` is a window whose whole contents are one
102+ratom, and a REPL that writes to it. Nothing in it is special to REPLs — it is
103+the ordinary arrangement with the cell driven by what you type rather than by a
104+handler — but it turns the toolkit into something you poke at rather than
105+restart:
106+
107+```sh
108+LD_LIBRARY_PATH=../build jolt repl-ui
109+```
110+
111+```clojure
112+ui=> (show! [:card {} [:title {:label "live"}] [:label {:label "typed just now"}]])
113+ui=> (show! (fn [] [:label {:label (str "hits " @hits)}])) ; a component, not a picture
114+ui=> (swap! hits inc) ; …so this re-renders it
115+ui=> (dump!)
116+ui=> :quit
117+```
118+
119+Hiccup is fixed at the moment you type it; a zero-arg function is mounted as a
120+component and re-runs whenever a cell it derefs changes. Either way the
121+reconciler patches what changed, so the window does not blink and an `:entry`
122+keeps its text and cursor across a `show!`.
123+
124+Two boundary rules shape the example, and both are the library's rather than
125+glimmer's:
126+
127+* **The frame loop must own the main thread.** winit refuses to create an event
128+ loop anywhere else, so `-main` paints on the main thread and reads stdin on a
129+ second one. From an editor it is the other way around — `jolt nrepl-server`
130+ parks the main thread in a pump, and `start!` posts the loop to it through
131+ `jolt.host/call-on-main-thread`, so eval stays free:
132+
133+ ```clojure
134+ (require '[glimmer-vidya.repl :as live])
135+ (live/start!)
136+ (live/show! [:label {:label "from the editor"}])
137+ ```
138+
139+ An event loop cannot be recreated either, so `stop!` ends the window for the
140+ life of that process and `start!` says so rather than opening an invisible
141+ one. Editing the UI never needs a restart; that is the point.
142+
143+* **Nodes belong to the loop thread.** A `show!` is safe from anywhere because
144+ a ratom is not a node — glimmer marshals the re-render through the backend's
145+ `schedule`. Anything that reads or writes the tree itself has to hop, which
146+ is what the example's `gui` helper does with `glimmer.core/on-gui` and why
147+ its `dump!` returns the tree rather than an empty root.
148+
98149 ## Seeing what was rendered
99150
100151 `dump` reads the tree back out of the library as hiccup — what is actually
@@ -93,8 +93,59 @@ jolt test # the suite, headless: no window, display or GPU needed
93 jolt counter # the counter above93 jolt counter # the counter above
94 jolt showcase # every tag, a keyed task list, an entry, a disabled subtree94 jolt showcase # every tag, a keyed task list, an entry, a disabled subtree
95 jolt smoke # non-interactive: reconciles under paint, then quits95 jolt smoke # non-interactive: reconciles under paint, then quits
96+jolt repl-ui # a window you edit from a prompt, live
96 ```97 ```
97 98
99+## Live from a REPL
100+
101+`examples/glimmer_vidya/repl.jolt` is a window whose whole contents are one
102+ratom, and a REPL that writes to it. Nothing in it is special to REPLs — it is
103+the ordinary arrangement with the cell driven by what you type rather than by a
104+handler — but it turns the toolkit into something you poke at rather than
105+restart:
106+
107+```sh
108+LD_LIBRARY_PATH=../build jolt repl-ui
109+```
110+
111+```clojure
112+ui=> (show! [:card {} [:title {:label "live"}] [:label {:label "typed just now"}]])
113+ui=> (show! (fn [] [:label {:label (str "hits " @hits)}])) ; a component, not a picture
114+ui=> (swap! hits inc) ; …so this re-renders it
115+ui=> (dump!)
116+ui=> :quit
117+```
118+
119+Hiccup is fixed at the moment you type it; a zero-arg function is mounted as a
120+component and re-runs whenever a cell it derefs changes. Either way the
121+reconciler patches what changed, so the window does not blink and an `:entry`
122+keeps its text and cursor across a `show!`.
123+
124+Two boundary rules shape the example, and both are the library's rather than
125+glimmer's:
126+
127+* **The frame loop must own the main thread.** winit refuses to create an event
128+ loop anywhere else, so `-main` paints on the main thread and reads stdin on a
129+ second one. From an editor it is the other way around — `jolt nrepl-server`
130+ parks the main thread in a pump, and `start!` posts the loop to it through
131+ `jolt.host/call-on-main-thread`, so eval stays free:
132+
133+ ```clojure
134+ (require '[glimmer-vidya.repl :as live])
135+ (live/start!)
136+ (live/show! [:label {:label "from the editor"}])
137+ ```
138+
139+ An event loop cannot be recreated either, so `stop!` ends the window for the
140+ life of that process and `start!` says so rather than opening an invisible
141+ one. Editing the UI never needs a restart; that is the point.
142+
143+* **Nodes belong to the loop thread.** A `show!` is safe from anywhere because
144+ a ratom is not a node — glimmer marshals the re-render through the backend's
145+ `schedule`. Anything that reads or writes the tree itself has to hop, which
146+ is what the example's `gui` helper does with `glimmer.core/on-gui` and why
147+ its `dump!` returns the tree rather than an empty root.
148+
98 ## Seeing what was rendered149 ## Seeing what was rendered
99 150
100 `dump` reads the tree back out of the library as hiccup — what is actually151 `dump` reads the tree back out of the library as hiccup — what is actually
modified jolt/glimmer-vidya/deps.edn +5 -2
@@ -25,9 +25,12 @@
2525 :showcase {:extra-paths ["examples"]
2626 :main-opts ["-m" "glimmer-vidya.showcase"]}
2727 :smoke {:extra-paths ["examples"]
28- :main-opts ["-m" "glimmer-vidya.smoke"]}}
28+ :main-opts ["-m" "glimmer-vidya.smoke"]}
29+ :repl-ui {:extra-paths ["examples"]
30+ :main-opts ["-m" "glimmer-vidya.repl"]}}
2931
3032 :tasks {test "jolt -M:test"
3133 counter "jolt -M:counter"
3234 showcase "jolt -M:showcase"
33- smoke "jolt -M:smoke"}}
35+ smoke "jolt -M:smoke"
36+ repl-ui "jolt -M:repl-ui"}}
@@ -25,9 +25,12 @@
25 :showcase {:extra-paths ["examples"]25 :showcase {:extra-paths ["examples"]
26 :main-opts ["-m" "glimmer-vidya.showcase"]}26 :main-opts ["-m" "glimmer-vidya.showcase"]}
27 :smoke {:extra-paths ["examples"]27 :smoke {:extra-paths ["examples"]
28- :main-opts ["-m" "glimmer-vidya.smoke"]}}28+ :main-opts ["-m" "glimmer-vidya.smoke"]}
29+ :repl-ui {:extra-paths ["examples"]
30+ :main-opts ["-m" "glimmer-vidya.repl"]}}
29 31
30 :tasks {test "jolt -M:test"32 :tasks {test "jolt -M:test"
31 counter "jolt -M:counter"33 counter "jolt -M:counter"
32 showcase "jolt -M:showcase"34 showcase "jolt -M:showcase"
33- smoke "jolt -M:smoke"}}35+ smoke "jolt -M:smoke"
36+ repl-ui "jolt -M:repl-ui"}}
added jolt/glimmer-vidya/examples/glimmer_vidya/repl.jolt +193 -0
new file mode 100644
@@ -0,0 +1,193 @@
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"))
new file mode 100644
@@ -0,0 +1,193 @@
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"))