-
Notifications
You must be signed in to change notification settings - Fork 1
/
core.clj
418 lines (378 loc) · 16.5 KB
/
core.clj
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
(ns mindra.core
(:require [clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as s]
[babashka.process :as bp]))
(def MINDRA-MODE-GLOSS-STATIC 0)
(def MINDRA-MODE-GLOSS-INTERACTIVE 1)
;;
;; Private functions first. All public functions are at the bottom.
;;
(defn- print-and-raise-mindra-failure
"Prints and raises a failure message from mindra."
[tag body]
(println body)
(throw (AssertionError.
(str "Unexpected message received from mindra, with tag '" tag "'."))))
(defn- read-message
"Reads a message sent by mindra."
[^java.io.BufferedReader reader]
(let [line (.readLine reader)]
(if (nil? line)
{}
(if (empty? line)
(read-message reader)
(let [cleaned-line (s/replace line "\\n" "\n")
splits (s/split cleaned-line #"\s+" 2)
tag (first splits)
body (second splits)]
{:tag tag :body body})))))
(defn- write-message
"Writes a message to mindra's stdin."
([^java.io.BufferedWriter writer tag] (write-message writer tag nil))
([^java.io.BufferedWriter writer tag body]
(if (nil? body)
(.write writer ^String tag)
(.write writer (str tag " " body)))
(.write writer (str \newline \newline))
(.flush writer)))
(defn- default-exit-event?
"Default predicate function to decide if an event is an exit-event.
ESC key is the default way to exit."
[event]
(and (= (:name event) "EventKey")
(= (first (:args event)) :specialKeyEsc)))
(defn- parse-event
"Parses an event message from mindra to a dict with :name and :args."
[message-body]
(let [splits (s/split message-body #"\s+")
event-name (first splits)
event-args (map edn/read-string (rest splits))]
{:name event-name
:args event-args}))
(defn- handle-event
"Handles an event message sent by mindra."
[writer configuration world event]
(if ((:exit-event? configuration) event)
(write-message writer "SHUTDOWN")
(do
((:on-event configuration) world event)
(write-message writer "OK"))))
(defn- handle-step
"Handles a step message sent by mindra."
[writer configuration world message-body]
(let [seconds (Float/parseFloat message-body)]
((:on-step configuration) world seconds)
(write-message writer "OK")))
(defn- handle-picture-request
"Handles a picture request message sent by mindra."
[writer configuration world]
(let [picture ((:world->picture configuration) world)]
(write-message writer "PICTURE" picture)))
(defn- handle-svg-request
"Handles a SVG request message sent by mindra."
[writer _configuration diagram]
(write-message writer "SVG" diagram))
(defn- handle-raster-request
"Handles a RASTER request message sent by mindra."
[writer _configuration diagram]
(write-message writer "RASTER" diagram))
(defn- handle-svg-init-request
"Handles initialization request from mindra for setting it up for SVG drawing."
[writer configuration]
(let [width (:width configuration)
height (:height configuration)
file-path (:file-path configuration)
file-path-str (if (nil? file-path) nil (pr-str file-path))]
(write-message writer
"INIT"
(s/join
" "
(list "Diagrams" "SVG" width height file-path-str)))))
(defn- handle-raster-init-request
"Handles initialization request from mindra for setting it up for SVG drawing."
[writer configuration]
(let [width (:width configuration)
height (:height configuration)
file-path (:file-path configuration)]
(write-message writer
"INIT"
(s/join
" "
(list "Diagrams" "Raster" width height (pr-str file-path))))))
(defn- handle-gloss-init-request
"Handles initialization request from mindra for setting it up for Gloss."
[writer configuration]
(let [mode (:mode configuration MINDRA-MODE-GLOSS-STATIC)
full-screen? (:full-screen? configuration)
window (:window configuration)
color (:background-color configuration)
steps-per-second (:steps-per-second configuration 50)
no-event (:no-event configuration)
no-step (:no-step configuration)
window-cfg (if full-screen?
"FullScreen"
(s/join " " (list "Window"
(:width window 512)
(:height window 512)
(:x window 10)
(:y window 10)
(pr-str (:title window "Mindra")))))]
(write-message writer
"INIT"
(s/join
" "
(list "Gloss"
"Mode"
mode
window-cfg
"Color"
(:red color 255)
(:green color 255)
(:blue color 255)
(:alpha color 255)
"StepsPerSecond"
steps-per-second
(when no-event "NoEvent")
(when no-step "NoStep"))))))
(defn- handle-shutdown
"Handles shutdown message from mindra."
[writer configuration world]
((:on-exit configuration) world)
(write-message writer "OK"))
(defn- handle-gloss-ready
"Handles ready message from mindra for Gloss."
[writer configuration world message-body]
(case message-body
"INIT" (handle-gloss-init-request writer configuration)
"PICTURE" (handle-picture-request writer configuration world)
(throw (AssertionError. (str "Unexpected READY message received: " message-body)))))
(defn- handle-svg-ready
"Handles ready message from mindra for SVG drawings."
[writer configuration svg message-body]
(case message-body
"INIT" (handle-svg-init-request writer configuration)
"SVG" (handle-svg-request writer configuration svg)
(throw (AssertionError. (str "Unexpected READY message received: " message-body)))))
(defn- handle-raster-ready
"Handles ready message from mindra for raster image drawings."
[writer configuration diagram message-body]
(case message-body
"INIT" (handle-raster-init-request writer configuration)
"RASTER" (handle-raster-request writer configuration diagram)
(throw (AssertionError. (str "Unexpected READY message received: " message-body)))))
(defn- print-mindra-startup-failure
"Prints a helpful message when mindra command fails to start."
[mindra-path]
(println (str "😱 Failed to start mindra from path '" mindra-path "'"))
(println
(str \newline
"Please install mindra and provide the correct path to it using "
":mindra-path option -- defaults to 'mindra'.")))
(defn- start-mindra-or-fail
"Starts mindra command from given command or fails with a helpful error message."
[path]
(try
(bp/process [path] {:shutdown bp/destroy})
(catch java.io.IOException e
(print-mindra-startup-failure path)
(throw e))))
(defn mindra-version
"Gets version of the installed mindra binary."
([] (mindra-version "mindra"))
([mindra-path]
(try
(-> (bp/process [mindra-path "-v"] {:out :string})
bp/check
:out
s/trim)
(catch java.io.IOException e
(print-mindra-startup-failure mindra-path)
(throw e)))))
(defn diagram->svg
"Converts a diagram into SVG string using mindra.
Options:
:mindra-path -- path to mindra command (defaults to 'mindra')
:width -- width of svg to create
:height -- height of svg to create
Returns SVG as a string."
[diagram & {:keys [mindra-path
width
height]
:or {mindra-path "mindra"
width 512
height 512}}]
(let [configuration {:mindra-path mindra-path
:width width
:height height}
p (start-mindra-or-fail mindra-path)
reader (io/reader (:out p))
writer (io/writer (:in p))]
(loop [message (read-message reader)]
(let [tag (:tag message)
message-body (:body message)]
(case tag
nil true
"READY" (handle-svg-ready writer configuration diagram message-body)
"SVG" true
(print-and-raise-mindra-failure tag message-body))
(if (and (not= tag "SVG") (.isAlive ^java.lang.Process (:proc p)))
(recur (read-message reader))
message-body)))))
(defn diagram->file
"Writes diagram to a file on disk.
The file-type is determined by the file-extension. The following
file-extensions are supported: svg, png, tif, bmp, jpg, pdf. If file-type
cannot be determined, mindra will assume png.
Returns path of file written to."
[diagram file-path & {:keys [mindra-path
width
height]
:or {mindra-path "mindra"
width 512
height 512}}]
(let [configuration {:mindra-path mindra-path
:width width
:height height
:file-path file-path}
p (start-mindra-or-fail mindra-path)
reader (io/reader (:out p))
writer (io/writer (:in p))
ext (last (s/split file-path #"\."))
ready-fn (if (= "svg" ext) handle-svg-ready handle-raster-ready)]
(loop [message (read-message reader)]
(let [tag (:tag message)
message-body (:body message)]
(case tag
nil true
"READY" (ready-fn writer configuration diagram message-body)
"SVG" true
"RASTER" true
(print-and-raise-mindra-failure tag message-body))
(if (and (not= tag "SVG")
(not= tag "RASTER")
(.isAlive ^java.lang.Process (:proc p)))
(recur (read-message reader))
file-path)))))
(defn gloss-draw
"Draws a gloss picture.
See https://hackage.haskell.org/package/gloss-1.13.2.1/docs/Graphics-Gloss-Interface-IO-Display.html
Options:
:mindra-path -- path to mindra command (defaults to 'mindra')
:full-screen? Whether gloss should start in full-screen mode
(defaults to false. If true, then :window option is ignored)
:window -- a map with :width, :height, :x, :y and :title keys for configuring the GUI window
(defaults to 512, 512, 10, 10, 'Mindra'. Ignored if :full-screen? is set to true)
:background-color -- a map with :red, :green, :blue, :alpha keys for setting the background color
(defaults to 255, 255, 255, 255)"
[picture & {:as provided-opts}]
(let [default-configuration {:mindra-path "mindra"
:full-screen? false
:window {:width 512
:height 512
:x 10
:y 10
:title "Mindra"}
:background-color {:red 255
:green 255
:blue 255
:alpha 255}
:world->picture (constantly picture)
:mode MINDRA-MODE-GLOSS-STATIC
:no-event false
:no-step false}
configuration (merge default-configuration provided-opts)
p (start-mindra-or-fail (:mindra-path configuration))
reader (io/reader (:out p))
writer (io/writer (:in p))
world (atom {})]
(loop [message (read-message reader)]
(let [tag (:tag message)
message-body (:body message)]
(case tag
nil true
"READY" (handle-gloss-ready writer configuration world message-body)
(print-and-raise-mindra-failure tag message-body))
(when (and (not= message-body "PHOTO") (.isAlive ^java.lang.Process (:proc p)))
(recur (read-message reader)))))))
(defn gloss-play
"Plays a gloss animation starting with given initial state (the world).
See https://hackage.haskell.org/package/gloss-1.13.2.1/docs/Graphics-Gloss-Interface-IO-Game.html
Options:
:mindra-path -- path to mindra command (defaults to 'mindra')
:on-step -- function that takes in world and number of seconds elasped; called on every step
:on-event -- function that takes in world and a map describing the event; called whenever an event fires
:on-exit -- function that takes in world; called when the gloss window is closed
:exit-event? -- a predicate function that takes in an event and decides if it is an exit event
(by default hitting `ESC` key is an exit event)
:world->picture -- a function that converts world to a gloss picture
(defaults to creating a rectangle of size 100 x 100)
:full-screen? Whether gloss should start in full-screen mode
(defaults to false. If true, then :window option is ignored)
:window -- a map with :width, :height, :x, :y and :title keys for configuring the GUI window
(defaults to 512, 512, 10, 10, 'Mindra'. Ignored if :full-screen? is set to true)
:steps-per-second -- number of steps per second (defaults to 50)"
[world & {:as provided-opts}]
(let [default-configuration {:mindra-path "mindra"
:on-step (constantly true)
:on-event #(println %2)
:on-exit (constantly true)
:exit-event? default-exit-event?
:world->picture (constantly (str "RectangleWire 100.0 100.0"))
:window {:width 512
:height 512
:x 10
:y 10
:title "Mindra"}
:background-color {:red 255
:green 255
:blue 255
:alpha 255}
:steps-per-second 50
:mode MINDRA-MODE-GLOSS-INTERACTIVE}
configuration (merge default-configuration provided-opts)
p (start-mindra-or-fail (:mindra-path configuration))
reader (io/reader (:out p))
writer (io/writer (:in p))]
(loop [message (read-message reader)]
(let [tag (:tag message)
message-body (:body message)]
(case tag
nil true
"READY" (handle-gloss-ready writer configuration world message-body)
"STEP" (handle-step writer configuration world message-body)
"EVENT" (handle-event writer configuration world (parse-event message-body))
"SHUTDOWN" (handle-shutdown writer configuration world)
(do
(write-message writer "SHUTDOWN")
(handle-shutdown writer configuration world)
(print-and-raise-mindra-failure tag message-body)))
(when (and (not= tag "SHUTDOWN") (.isAlive ^java.lang.Process (:proc p)))
(recur (read-message reader)))))))
(defn show-diagram
"A convenience function to show a diagram in a gloss window.
Requires mindra v0.0.3 or higher.
It writes a bmp file to a temp file and then loads it in gloss window.
This function accepts the same options as gloss-draw function."
[diagram & {:keys [mindra-path
full-screen?
window
background-color]
:or {mindra-path "mindra"
full-screen? false
window {:width 512 :height 512 :x 10 :y 10 :title "Mindra"}
background-color {:red 255 :green 255 :blue 255 :alpha 255}}}]
(let [tmp-file (java.io.File/createTempFile "mindra" ".bmp")
_ (.deleteOnExit tmp-file)
tmp-file-path (.getAbsolutePath tmp-file)
file-path (diagram->file diagram tmp-file-path
:mindra-path mindra-path
:width (:width window)
:height (:height window))
picture (str "Image" " " (pr-str file-path))]
(gloss-draw picture
:mindra-path mindra-path
:full-screen? full-screen?
:window window
:background-color background-color)
(.delete tmp-file)))