1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
|
(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"))
|