Say you want to quickly show some custom information in a nice tabular format, and decide that an HTML/SVG page would do the job. Since there are a lot of rows, you write a script to generate them instead of writing lots of tags. After wrapping up the output in some extra HTML, now you have a nice page you can view in the browser.
Now let's say you want to iterate on the formatting and styling of the page. You could tweak the code, maybe rerun your script, and refresh. To speed that up, you could use a live reload browser extension. To automate the browser reloading whenever you make changes, you could bring in a framework like next.js.
I wanted these conveniences, but something lighter. Here is a handy method I've been using in the recent months.
There are 2 core components:
- a container HTML page that connects to a websocket
- websocat for data relay (this program is ridiculously useful)
After that, you just send the HTML you want to display (a file, script, curl output, whatever) to websocat. For refresh-on-save, you can use watchexec or a file monitor of your choice, like entr, or plain looping in a script.
this example contains the bare minimum logic of connecting to a websocket and updating a DOM container.
<!-- display.html -->
<html>
<body>
<div id="container"></div>
<script>
$container = document.getElementById('container')
const connectLoop = () => {
const connection = new WebSocket(`ws://localhost:9999`)
connection.onopen = (event) => {
console.info('socket connected')
}
connection.onclose = () => {
console.warn(`socket closed; retrying`)
setTimeout(connectLoop, 5000)
}
connection.onmessage = function (event) {
let message = (event.data ?? '').trim()
let newElement = document.createElement('div')
newElement.innerHTML = message
$container.innerHTML = ''
$container.appendChild(newElement)
};
}
connectLoop()
</script>
</body>
</html>
websocat --buffer-size 99999999 --text ws-l:127.0.0.1:9999 broadcast:mirror: --exit-on-eof
once connected, whatever you send to port 9999 will get displayed in the browser
if your HTML is from a file on disk, you can simply pipe it into websocat:
cat your-file.html | websocat ws://localhost:9999 --buffer-size 99999999
We can use hiccup
in babashka to push frames as an animation:
;; mypage.bb.clj
(ns mypage
(:require [babashka.process :as p :refer [process]]
[hiccup2.core :as h]))
(defrecord Circle
[radius
color
step-size
x x-velocity
y y-velocity
])
(defn send-output! [payload number]
(if nil ;; write to file instead?
(spit (format "frame-%03d.svg" number) payload)
(process ['websocat "ws://localhost:9999" "--buffer-size" 99999999]
{:in payload})))
(let [$steps 333
$width 900
$height 700
$circles (concat
[(->Circle 50 "pink" 20 (* $width 0.1) 1.2 (* $height 0.2) 1.0)
(->Circle 30 "olive" 10 (* $width 0.7) 0.8 (* $height 0.6) 1.3)
(->Circle 20 "skyblue" 10 (* $width 0.4) 1.4 (* $height 0.3) 1.2)]
(->> (range 99)
(map (fn [_]
(->Circle 15 "#cccccc" 15 (* $width 0.05) (rand) (* $height 0.95) (rand))))))
out-of-bounds? (fn [min-position max-position velocity position radius]
(or (and (< velocity 0) (< position (+ min-position radius)))
(and (> velocity 0) (< (- max-position radius) position))))]
(loop [remain-steps (range $steps)
circles $circles]
(when-let [step (first remain-steps)]
(-> [:svg
{:xmlns "http://www.w3.org/2000/svg"
:xmlns:xlink "http://www.w3.org/1999/xlink"
:width $width
:height $height}
[:g
(->> circles
(map (fn [{:keys [x y radius color]}]
[:circle
{:cx x
:cy y
:r radius
:fill-opacity 0.5
:fill color}])))]]
(h/html)
(send-output! step))
(recur (rest remain-steps)
;; next step's circles
(->> circles
(map (fn [{:keys [x y step-size radius x-velocity y-velocity] :as circle}]
(let [next-x (+ x (* step-size x-velocity))
next-y (+ y (* step-size y-velocity))]
(assoc circle
:x next-x
:x-velocity (if (out-of-bounds? 0 $width x-velocity next-x radius)
(- x-velocity)
x-velocity)
:y next-y
:y-velocity (if (out-of-bounds? 0 $height y-velocity next-y radius)
(- y-velocity)
y-velocity))))))))))
the combined output above was generated using svgasm:
svgasm -d 0.02 -i 99 frame-*.svg -o animated.svg
watchexec -r -w . -- bb mypage.bb.clj
This isn't quite like the REPL-driven live coding that is enabled by figwheel, but for small pages, the push-on-save feels instantaneous, making for a tight code-to-display feedback loop.