First commit
This commit is contained in:
commit
02359b3799
34
.dir-locals.el
Normal file
34
.dir-locals.el
Normal file
@ -0,0 +1,34 @@
|
||||
;;; Directory Local Variables -*- no-byte-compile: t -*-
|
||||
|
||||
((clojure-mode
|
||||
. ((cider-clojure-cli-parameters . "-M:common:dev")
|
||||
;; Otherwise cider-jack-in appends its own -Sdeps '{...}' blob with
|
||||
;; nrepl/cider-nrepl regardless of cli-parameters above -- deps.edn's
|
||||
;; :dev alias already provides those, so skip the duplicate injection.
|
||||
(cider-inject-dependencies-at-jack-in . nil)
|
||||
(eval
|
||||
. (progn
|
||||
(unless (fboundp 'clj-watch)
|
||||
(load (expand-file-name
|
||||
"watch.el"
|
||||
(locate-dominating-file default-directory ".dir-locals.el"))))
|
||||
|
||||
(defun siam-farmer-run-main ()
|
||||
"Send (future (-main)) to the REPL."
|
||||
(interactive)
|
||||
(cider-interactive-eval "(future (-main))"))
|
||||
|
||||
(unless (boundp 'siam-farmer-mode-map)
|
||||
(defvar siam-farmer-mode-map
|
||||
(let ((m (make-sparse-keymap)))
|
||||
(define-key m (kbd "C-c C-w") #'clj-watch)
|
||||
(define-key m (kbd "C-c C-M-r") #'siam-farmer-run-main)
|
||||
m)))
|
||||
|
||||
(unless (fboundp 'siam-farmer-mode)
|
||||
(define-minor-mode siam-farmer-mode
|
||||
"Project keybindings for siam-farmer."
|
||||
:lighter " SF"
|
||||
:keymap siam-farmer-mode-map))
|
||||
|
||||
(siam-farmer-mode 1))))))
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/.cpcache/
|
||||
/.nrepl-port
|
||||
18
deps.edn
Normal file
18
deps.edn
Normal file
@ -0,0 +1,18 @@
|
||||
{:paths ["src"]
|
||||
:deps {org.clojure/clojure {:mvn/version "1.12.1"}
|
||||
org.suskalo/coffi {:mvn/version "1.0.615"}}
|
||||
|
||||
:aliases
|
||||
{:common
|
||||
{:jvm-opts ["--enable-native-access=ALL-UNNAMED"
|
||||
"-Draylib.path=/home/joe/.local/bin/odin/vendor/raylib/linux/libraylib.so.600"]}
|
||||
:run
|
||||
{:jvm-opts ["--enable-native-access=ALL-UNNAMED"
|
||||
"-Draylib.path=/home/joe/.local/bin/odin/vendor/raylib/linux/libraylib.so.600"]
|
||||
:main-opts ["-m" "game"]}
|
||||
|
||||
:dev
|
||||
{:extra-deps {nrepl/nrepl {:mvn/version "1.7.0"}
|
||||
cider/cider-nrepl {:mvn/version "0.62.2"}}
|
||||
:main-opts ["-m" "nrepl.cmdline"
|
||||
"--middleware" "[cider.nrepl/cider-middleware]"]}}}
|
||||
1
game-config.edn
Normal file
1
game-config.edn
Normal file
@ -0,0 +1 @@
|
||||
{:foo :bar}
|
||||
42
src/engine.clj
Normal file
42
src/engine.clj
Normal file
@ -0,0 +1,42 @@
|
||||
(ns engine
|
||||
(:require
|
||||
[clojure.edn :as edn]
|
||||
[rl :as rl]
|
||||
[watch :as w]))
|
||||
|
||||
(defonce ^:private last-error (atom nil))
|
||||
|
||||
(reset! last-error (ex-info "something bad happened" {:reason "why"}))
|
||||
(reset! last-error nil)
|
||||
|
||||
(defn- draw-error! [err]
|
||||
(rl/clear-background!* rl/cornflower-blue)
|
||||
(rl/draw-rectangle!* 10 10 300 30 (rl/color-seg 0xFF0000))
|
||||
(rl/draw-text!* "Fix the code then left click anywhere with the mouse" 0 10 16 rl/white)
|
||||
#_(rl/draw-text!* (str err) 50 100 20 rl/white)
|
||||
(rl/draw-text!* "Error" 50 50 40 rl/white))
|
||||
|
||||
(defn run-game!
|
||||
[{:keys [title width height config-path init input update draw watch-fn]
|
||||
:or {title "Untitled" width 1200 height 900 watch-fn (constantly nil)}}]
|
||||
(rl/set-trace-log-level! rl/log-warning)
|
||||
(rl/init-window! width height title)
|
||||
(rl/set-target-fps! 60)
|
||||
(let [state (atom (init (edn/read-string (slurp config-path))))]
|
||||
(while (not (rl/window-should-close?))
|
||||
(rl/with-drawing!
|
||||
(if @last-error
|
||||
(do
|
||||
(draw-error! @last-error)
|
||||
(when (rl/mouse-button-released? rl/mouse-button-left)
|
||||
(reset! last-error nil)))
|
||||
(try
|
||||
(swap! state input)
|
||||
(swap! state update)
|
||||
(draw @state)
|
||||
(w/watch! (watch-fn @state))
|
||||
;; TODO: Have Cider report this exception
|
||||
(catch Throwable t
|
||||
(println t)
|
||||
(reset! last-error t))))))
|
||||
(rl/close-window!)))
|
||||
40
src/game.clj
Normal file
40
src/game.clj
Normal file
@ -0,0 +1,40 @@
|
||||
(ns game
|
||||
(:require
|
||||
[engine]
|
||||
[rl :as rl]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
;; (set! *unchecked-math* :warn-on-boxed)
|
||||
|
||||
(defn init-game [config]
|
||||
{:color-idx 0})
|
||||
|
||||
(defn handle-game-input [game]
|
||||
(when (rl/key-pressed? rl/key-r))
|
||||
(when (rl/mouse-button-down? rl/mouse-button-left))
|
||||
game)
|
||||
|
||||
(defn update-game [game]
|
||||
game)
|
||||
|
||||
(defn draw-game [game]
|
||||
(rl/clear-background!* rl/cornflower-blue))
|
||||
|
||||
(defn watch-game [game]
|
||||
{:color-idx (:color-idx game)})
|
||||
|
||||
(defn -main [& _args]
|
||||
(engine/run-game!
|
||||
{:title "Siam Farmer"
|
||||
:width 900
|
||||
:height 500
|
||||
:config-path "game-config.edn"
|
||||
:init #'init-game
|
||||
:input #'handle-game-input
|
||||
:update #'update-game
|
||||
:draw #'draw-game
|
||||
:watch-fn #'watch-game}))
|
||||
|
||||
(comment
|
||||
;;
|
||||
:-)
|
||||
207
src/rl.clj
Normal file
207
src/rl.clj
Normal file
@ -0,0 +1,207 @@
|
||||
(ns rl
|
||||
"Hand-written raylib bindings via coffi/Panama. Only what sand needs.
|
||||
|
||||
Struct layouts are written against raylib 6.0's raylib.h -- a mismatch here is
|
||||
silent memory corruption, not an error, so check src/raylib.h before bumping."
|
||||
(:require
|
||||
[coffi.mem :as mem :refer [defalias]]
|
||||
[coffi.ffi :as ffi :refer [defcfn]])
|
||||
(:import
|
||||
(java.lang.foreign Arena MemorySegment)))
|
||||
|
||||
(ffi/load-library (or (System/getProperty "raylib.path")
|
||||
"libraylib.so.600"))
|
||||
|
||||
;;; ---------------------------------------------------------------- primitives
|
||||
;; coffi's ::mem/byte is signed; raylib's Color fields are unsigned char, so 230
|
||||
;; would overflow on the way in. Round-trip through unchecked-byte instead.
|
||||
|
||||
(defmethod mem/primitive-type ::ubyte [_type] ::mem/byte)
|
||||
(defmethod mem/serialize* ::ubyte [obj _type _scope] (unchecked-byte obj))
|
||||
(defmethod mem/deserialize* ::ubyte [obj _type] (Byte/toUnsignedLong obj))
|
||||
|
||||
;; C bool is one byte.
|
||||
(defmethod mem/primitive-type ::bool [_type] ::mem/byte)
|
||||
(defmethod mem/serialize* ::bool [obj _type _scope] (byte (if obj 1 0)))
|
||||
(defmethod mem/deserialize* ::bool [obj _type] (not (zero? obj)))
|
||||
|
||||
;;; ------------------------------------------------------------------- structs
|
||||
|
||||
(defalias ::color
|
||||
[::mem/struct [[:r ::ubyte] [:g ::ubyte] [:b ::ubyte] [:a ::ubyte]]])
|
||||
|
||||
(defalias ::vector-2
|
||||
[::mem/struct [[:x ::mem/float] [:y ::mem/float]]])
|
||||
|
||||
(defalias ::rectangle
|
||||
[::mem/struct [[:x ::mem/float] [:y ::mem/float]
|
||||
[:width ::mem/float] [:height ::mem/float]]])
|
||||
|
||||
(defalias ::texture
|
||||
[::mem/struct [[:id ::mem/int] [:width ::mem/int] [:height ::mem/int]
|
||||
[:mipmaps ::mem/int] [:format ::mem/int]]])
|
||||
|
||||
(defalias ::image
|
||||
[::mem/struct [[:data ::mem/pointer] [:width ::mem/int] [:height ::mem/int]
|
||||
[:mipmaps ::mem/int] [:format ::mem/int]]])
|
||||
|
||||
(defalias ::font
|
||||
[::mem/struct [[:base-size ::mem/int] [:glyph-count ::mem/int] [:glyph-padding ::mem/int]
|
||||
[:texture ::texture] [:recs ::mem/pointer] [:glyphs ::mem/pointer]]])
|
||||
|
||||
;;; ----------------------------------------------------------------- constants
|
||||
|
||||
(def ^:const key-r 82)
|
||||
(def ^:const mouse-button-left 0)
|
||||
(def ^:const log-warning 4)
|
||||
(def ^:const pixelformat-r8g8b8a8 7)
|
||||
(def ^:const texture-filter-point 0)
|
||||
|
||||
;;; ----------------------------------------------------------------- functions
|
||||
|
||||
(defcfn init-window! "InitWindow" [::mem/int ::mem/int ::mem/c-string] ::mem/void)
|
||||
(defcfn close-window! "CloseWindow" [] ::mem/void)
|
||||
(defcfn set-target-fps! "SetTargetFPS" [::mem/int] ::mem/void)
|
||||
(defcfn set-trace-log-level! "SetTraceLogLevel" [::mem/int] ::mem/void)
|
||||
(defcfn window-should-close? "WindowShouldClose" [] ::bool)
|
||||
|
||||
(defcfn begin-drawing! "BeginDrawing" [] ::mem/void)
|
||||
(defcfn end-drawing! "EndDrawing" [] ::mem/void)
|
||||
|
||||
(defcfn draw-fps "DrawFPS" [::mem/int ::mem/int] ::mem/void)
|
||||
(defcfn get-fps "GetFPS" [] ::mem/int)
|
||||
|
||||
(defcfn key-pressed? "IsKeyPressed" [::mem/int] ::bool)
|
||||
(defcfn mouse-button-down? "IsMouseButtonDown" [::mem/int] ::bool)
|
||||
(defcfn mouse-button-released? "IsMouseButtonReleased" [::mem/int] ::bool)
|
||||
(defcfn get-mouse-position "GetMousePosition" [] ::vector-2)
|
||||
|
||||
;; The !* forms take pre-serialized struct segments and allocate nothing per
|
||||
;; call. Everything in a per-frame path uses these.
|
||||
(def clear-background!*
|
||||
(ffi/make-downcall "ClearBackground" [::color] ::mem/void))
|
||||
|
||||
(def ^:private draw-rectangle-raw!*
|
||||
(ffi/make-downcall "DrawRectangle"
|
||||
[::mem/int ::mem/int ::mem/int ::mem/int ::color] ::mem/void))
|
||||
|
||||
(defn draw-rectangle!*
|
||||
[x y w h color]
|
||||
(draw-rectangle-raw!* (int x) (int y) (int w) (int h) color))
|
||||
|
||||
(def update-texture!*
|
||||
(ffi/make-downcall "UpdateTexture" [::texture ::mem/pointer] ::mem/void))
|
||||
|
||||
(def draw-texture-pro!*
|
||||
(ffi/make-downcall "DrawTexturePro"
|
||||
[::texture ::rectangle ::rectangle ::vector-2 ::mem/float ::color]
|
||||
::mem/void))
|
||||
|
||||
;; Called once at startup, so the map-taking form is fine.
|
||||
(defcfn load-texture-from-image "LoadTextureFromImage" [::image] ::texture)
|
||||
(defcfn unload-texture! "UnloadTexture" [::texture] ::mem/void)
|
||||
(defcfn set-texture-filter! "SetTextureFilter" [::texture ::mem/int] ::mem/void)
|
||||
(defcfn get-font-default "GetFontDefault" [] ::font)
|
||||
|
||||
(defcfn measure-text "MeasureText" [::mem/c-string ::mem/int] ::mem/int)
|
||||
|
||||
(def ^:private draw-text-raw!*
|
||||
(ffi/make-downcall "DrawText"
|
||||
[::mem/c-string ::mem/int ::mem/int ::mem/int ::color] ::mem/void))
|
||||
|
||||
(def ^:private draw-text-ex-raw!*
|
||||
(ffi/make-downcall "DrawTextEx"
|
||||
[::font ::mem/c-string ::vector-2 ::mem/float ::mem/float ::color]
|
||||
::mem/void))
|
||||
|
||||
(def ^:private measure-text-ex-raw!*
|
||||
(ffi/make-downcall "MeasureTextEx"
|
||||
[::font ::mem/c-string ::mem/float ::mem/float] ::vector-2))
|
||||
|
||||
;;; --------------------------------------------------- macros
|
||||
|
||||
(defmacro with-drawing! [& body]
|
||||
`(do
|
||||
(try
|
||||
(begin-drawing!)
|
||||
~@body
|
||||
(end-drawing!))))
|
||||
|
||||
;;; --------------------------------------------------- native value allocation
|
||||
|
||||
(defonce ^Arena arena (Arena/ofAuto))
|
||||
|
||||
(defn color-seg
|
||||
"Serialize a packed 0xRRGGBB int into a reusable native Color, once."
|
||||
[^long hex]
|
||||
(mem/serialize {:r (bit-and (bit-shift-right hex 16) 0xFF)
|
||||
:g (bit-and (bit-shift-right hex 8) 0xFF)
|
||||
:b (bit-and hex 0xFF)
|
||||
:a 255}
|
||||
::color arena))
|
||||
|
||||
(def black (color-seg 0x000000))
|
||||
(def white (color-seg 0xFFFFFF))
|
||||
(def cornflower-blue (color-seg 0x6495ED))
|
||||
|
||||
(defn rect-seg [x y w h]
|
||||
(mem/serialize {:x (float x) :y (float y) :width (float w) :height (float h)}
|
||||
::rectangle arena))
|
||||
|
||||
(defn vec2-seg [x y]
|
||||
(mem/serialize {:x (float x) :y (float y)} ::vector-2 arena))
|
||||
|
||||
(defn texture-seg [tex]
|
||||
(mem/serialize tex ::texture arena))
|
||||
|
||||
(defn font-seg [font]
|
||||
(mem/serialize font ::font arena))
|
||||
|
||||
;; GetFontDefault needs a window/GL context, so this is a fn, not a def --
|
||||
;; call it once after init-window! and hang onto the result.
|
||||
(defn default-font-seg []
|
||||
(font-seg (get-font-default)))
|
||||
|
||||
;; The raw !* downcalls above take fully native args -- no auto marshaling,
|
||||
;; unlike defcfn. text/x/y change every call anyway (so a per-call c-string
|
||||
;; alloc is unavoidable), but color/font are expected pre-serialized (rl/white,
|
||||
;; a font-seg) same as the other !* draw calls.
|
||||
|
||||
(defn draw-text!*
|
||||
[text x y font-size color]
|
||||
(draw-text-raw!* (mem/serialize text ::mem/c-string arena)
|
||||
(int x) (int y) (int font-size) color))
|
||||
|
||||
(defn draw-text-ex!*
|
||||
[font text x y font-size spacing color]
|
||||
(draw-text-ex-raw!* font
|
||||
(mem/serialize text ::mem/c-string arena)
|
||||
(vec2-seg x y)
|
||||
(float font-size) (float spacing) color))
|
||||
|
||||
(defn measure-text-ex!*
|
||||
"Returns a struct by value, so the raw downcall needs an allocator (arena)
|
||||
as its first arg to write the result into, and the segment it hands back
|
||||
needs an explicit deserialize -- unlike defcfn, make-downcall doesn't do
|
||||
either automatically."
|
||||
[font text font-size spacing]
|
||||
(mem/deserialize
|
||||
(measure-text-ex-raw!* arena font
|
||||
(mem/serialize text ::mem/c-string arena)
|
||||
(float font-size) (float spacing))
|
||||
::vector-2))
|
||||
|
||||
(defn alloc-pixels
|
||||
"An RGBA8888 pixel buffer, native so UpdateTexture can read it directly."
|
||||
^MemorySegment [^long n-pixels]
|
||||
(.allocate arena (* 4 n-pixels) 4))
|
||||
|
||||
(defn rgba-le
|
||||
"0xRRGGBB -> an int whose little-endian bytes are R,G,B,A, matching
|
||||
PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 in memory."
|
||||
^long [^long hex]
|
||||
(unchecked-int
|
||||
(bit-or (bit-and (bit-shift-right hex 16) 0xFF)
|
||||
(bit-shift-left (bit-and (bit-shift-right hex 8) 0xFF) 8)
|
||||
(bit-shift-left (bit-and hex 0xFF) 16)
|
||||
(bit-shift-left 0xFF 24))))
|
||||
121
src/watch.clj
Normal file
121
src/watch.clj
Normal file
@ -0,0 +1,121 @@
|
||||
(ns watch
|
||||
"A pull-based watch: the game drops a snapshot into an atom, Emacs polls it on
|
||||
its own timer. Nothing is pushed over nREPL, so the REPL stays clean and the
|
||||
watch rate is decoupled from the frame rate."
|
||||
(:require [clojure.string :as str])
|
||||
(:import (java.util.concurrent ConcurrentHashMap)))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
;; Compile-time flag. -Dwatch.dev=false makes every macro here expand to nothing
|
||||
;; (spy forms collapse back to the bare expression), so a production build
|
||||
;; carries no cost at all.
|
||||
(def ^:const enabled? (not= "false" (System/getProperty "watch.dev" "true")))
|
||||
|
||||
(defonce values (atom {}))
|
||||
|
||||
(defmacro watch!
|
||||
"Publish a map of label -> value for the pinned watch buffer.
|
||||
|
||||
Call this once per frame, never inside a hot inner loop -- one reset! of the
|
||||
whole map is 120/sec and free, while per-key swap!s both allocate in the loop
|
||||
and let the reader see a torn, half-updated frame."
|
||||
[m]
|
||||
(when enabled?
|
||||
`(reset! values ~m)))
|
||||
|
||||
;;; --------------------------------------------------------------------- spy
|
||||
|
||||
(defonce ^ConcurrentHashMap spied (ConcurrentHashMap.))
|
||||
|
||||
(defn spy* [label v]
|
||||
(.put spied label v)
|
||||
v)
|
||||
|
||||
(defmacro spy
|
||||
"Record the value of expr under label and return it unchanged, so you can wrap
|
||||
an expression in place without restructuring the code:
|
||||
|
||||
(let [target (spy :target (min (dec rows) (+ row (long vv))))]
|
||||
...)
|
||||
|
||||
Last write wins. That is fine per frame, but from a hot loop you only ever see
|
||||
whichever cell happened to run last -- use spy-long there instead."
|
||||
[label expr]
|
||||
(if enabled?
|
||||
`(spy* ~label ~expr)
|
||||
expr))
|
||||
|
||||
;;; Numeric spy for hot loops. State lives in a double-array per label -- no
|
||||
;;; boxing, no allocation, so this survives being called thousands of times a
|
||||
;;; frame. Slots: 0 count, 1 min, 2 max, 3 last, 4 sum.
|
||||
(defonce ^ConcurrentHashMap counters (ConcurrentHashMap.))
|
||||
|
||||
(defn slot ^doubles [label]
|
||||
(or (.get counters label)
|
||||
(.computeIfAbsent counters label
|
||||
(reify java.util.function.Function
|
||||
(apply [_ _]
|
||||
(double-array [0 Double/POSITIVE_INFINITY
|
||||
Double/NEGATIVE_INFINITY 0 0]))))))
|
||||
|
||||
(defn record! [^doubles s ^double v]
|
||||
(aset s 0 (unchecked-inc (aget s 0)))
|
||||
(when (< v (aget s 1)) (aset s 1 v))
|
||||
(when (> v (aget s 2)) (aset s 2 v))
|
||||
(aset s 3 v)
|
||||
(aset s 4 (unchecked-add (aget s 4) v))
|
||||
nil)
|
||||
|
||||
(defmacro spy-num
|
||||
"Like spy, but for a numeric expression in a hot loop. Accumulates
|
||||
count/min/max/last/mean instead of keeping one sample, which is what you
|
||||
actually want when the expression runs thousands of times per frame.
|
||||
|
||||
Works on longs, doubles and floats alike. Note it returns the *original*
|
||||
value, not a coerced one -- wrapping a float in something that hands back a
|
||||
long silently truncates it and changes what the surrounding code computes."
|
||||
[label expr]
|
||||
(if enabled?
|
||||
`(let [v# ~expr]
|
||||
(record! (slot ~label) (double v#))
|
||||
v#)
|
||||
expr))
|
||||
|
||||
(defn reset-spies!
|
||||
"Clear accumulated spy state. Stats are cumulative until you call this."
|
||||
[]
|
||||
(.clear spied)
|
||||
(.clear counters))
|
||||
|
||||
;;; ------------------------------------------------------------------ render
|
||||
|
||||
(defn- fmt-num [^double d]
|
||||
;; Print whole numbers as integers so a spy on an index doesn't read as 66.00.
|
||||
(if (== d (Math/rint d)) (str (long d)) (format "%.4f" d)))
|
||||
|
||||
(defn- fmt-slot [^doubles s]
|
||||
(let [n (aget s 0)]
|
||||
(if (zero? n)
|
||||
"(no samples)"
|
||||
(format "n=%d min=%s max=%s last=%s mean=%s"
|
||||
(long n) (fmt-num (aget s 1)) (fmt-num (aget s 2))
|
||||
(fmt-num (aget s 3)) (fmt-num (/ (aget s 4) n))))))
|
||||
|
||||
(defn render
|
||||
"Format the current snapshot as plain text. Emacs calls this, not you."
|
||||
[]
|
||||
;; grid is 91,200 ints -- watch it by accident without these bound and the
|
||||
;; render hangs instead of printing.
|
||||
(binding [*print-length* 20
|
||||
*print-level* 3]
|
||||
(let [rows (concat (for [[k v] @values] [(str k) (pr-str v)])
|
||||
(for [[k v] (into {} spied)] [(str k) (pr-str v)])
|
||||
(for [[k v] (into {} counters)] [(str k) (fmt-slot v)]))
|
||||
rows (sort-by first rows)
|
||||
w (reduce max 1 (map (comp count first) rows))]
|
||||
(if (empty? rows)
|
||||
"(nothing watched)"
|
||||
(str/join "\n"
|
||||
(for [[k v] rows]
|
||||
(format (str "%-" w "s %s") k v)))))))
|
||||
76
watch.el
Normal file
76
watch.el
Normal file
@ -0,0 +1,76 @@
|
||||
;;; clj-watch.el --- A pinned, self-overwriting watch buffer -*- lexical-binding: t -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Polls `watch/render' on a timer and replaces the buffer contents in
|
||||
;; place. Unlike `cider-tap', nothing is appended -- the buffer always shows
|
||||
;; the current frame's snapshot and nothing else.
|
||||
;;
|
||||
;; Usage: M-x clj-watch / M-x clj-watch-stop
|
||||
;;
|
||||
;; Load with: (load "/home/joe/Development/siam-farmer/watch.el")
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cider-client)
|
||||
|
||||
(defvar clj-watch-buffer "*clj-watch*")
|
||||
(defvar clj-watch-interval 0.2
|
||||
"Seconds between polls. This is the watch rate, not the frame rate.")
|
||||
|
||||
(defvar clj-watch--timer nil)
|
||||
|
||||
(defun clj-watch--paint (text)
|
||||
"Replace the watch buffer's contents with TEXT."
|
||||
(when-let* ((buf (get-buffer clj-watch-buffer)))
|
||||
(let ((tmp (get-buffer-create " *clj-watch-src*")))
|
||||
(with-current-buffer tmp
|
||||
(erase-buffer)
|
||||
(insert text))
|
||||
(with-current-buffer buf
|
||||
(let ((inhibit-read-only t))
|
||||
;; replace-buffer-contents diffs rather than erasing, so point and
|
||||
;; scroll position survive every tick. erase-buffer + insert would
|
||||
;; yank the cursor back to the top five times a second.
|
||||
(replace-buffer-contents tmp))))))
|
||||
|
||||
(defun clj-watch--tick ()
|
||||
"Poll the snapshot once, asynchronously."
|
||||
(if (not (get-buffer clj-watch-buffer))
|
||||
(clj-watch-stop)
|
||||
;; Async, not `cider-nrepl-sync-request': a sync request on a timer blocks
|
||||
;; Emacs's UI thread every tick.
|
||||
(cider-nrepl-request:eval
|
||||
"(watch/render)"
|
||||
(lambda (response)
|
||||
(nrepl-dbind-response response (value err)
|
||||
(cond
|
||||
(err (clj-watch--paint (format "error:\n%s" err)))
|
||||
(value (clj-watch--paint (car (read-from-string value))))))))))
|
||||
|
||||
(define-derived-mode clj-watch-mode special-mode "clj-watch"
|
||||
"Major mode for the pinned watch buffer."
|
||||
(setq-local truncate-lines t))
|
||||
|
||||
;;;###autoload
|
||||
(defun clj-watch ()
|
||||
"Open the pinned watch buffer and start polling."
|
||||
(interactive)
|
||||
(cider-current-repl nil 'ensure)
|
||||
(with-current-buffer (get-buffer-create clj-watch-buffer)
|
||||
(unless (eq major-mode 'clj-watch-mode)
|
||||
(clj-watch-mode)))
|
||||
(when clj-watch--timer (cancel-timer clj-watch--timer))
|
||||
(setq clj-watch--timer
|
||||
(run-with-timer 0 clj-watch-interval #'clj-watch--tick))
|
||||
(display-buffer clj-watch-buffer))
|
||||
|
||||
(defun clj-watch-stop ()
|
||||
"Stop polling."
|
||||
(interactive)
|
||||
(when clj-watch--timer
|
||||
(cancel-timer clj-watch--timer)
|
||||
(setq clj-watch--timer nil)))
|
||||
|
||||
(provide 'clj-watch)
|
||||
;;; clj-watch.el ends here
|
||||
Loading…
x
Reference in New Issue
Block a user