├── demo-bb ├── bb.edn ├── app.el └── src │ └── app.clj ├── .gitignore ├── demo ├── deps.edn ├── .dir-locals.el ├── dev │ └── user.clj ├── app.el └── src │ └── app.clj ├── deps.edn ├── src ├── cloel │ └── repl.clj └── cloel.clj ├── pom.xml ├── new ├── README.zh-CN.md ├── cloel_test.el ├── README.md └── cloel.el /demo-bb/bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :deps {io.github.manateelazycat.cloel/cloel {:mvn/version "1.0.0"}} 4 | 5 | :tasks 6 | {cloel {:requires ([app]) 7 | :task (app/-main)}}} 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | *.pyc 3 | /.log/ 4 | node_modules/ 5 | dist/ 6 | tags 7 | test.log 8 | workspace/ 9 | .clj-kondo 10 | .lsp 11 | .cpcache 12 | target 13 | .nrepl-port 14 | .cloel-port 15 | -------------------------------------------------------------------------------- /demo/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {io.github.manateelazycat.cloel/cloel {:mvn/version "1.0.0"}} 2 | 3 | :paths ["src"] 4 | 5 | :aliases 6 | {:cloel {:main-opts ["-m" "app"]} 7 | :dev {:extra-paths ["dev"]}}} 8 | -------------------------------------------------------------------------------- /demo/.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;;; Directory Local Variables -*- no-byte-compile: t -*- 2 | ;;; For more information see (info "(emacs) Directory Variables") 3 | 4 | ((nil . ((cider-clojure-cli-aliases . "dev")))) 5 | -------------------------------------------------------------------------------- /demo/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require cloel.repl)) 3 | 4 | (defn start [] 5 | (cloel.repl/start-server)) 6 | 7 | (defn stop [] 8 | (cloel.repl/stop-server)) 9 | 10 | (start) 11 | 12 | (comment 13 | (start) 14 | (stop)) 15 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}} 3 | :aliases 4 | {:jar {:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.1.303"}} 5 | :exec-fn hf.depstar/jar 6 | :exec-args {:jar "target/cloel.jar" :sync-pom true}} 7 | :install {:replace-deps {slipset/deps-deploy {:mvn/version "0.2.0"}} 8 | :exec-fn deps-deploy.deps-deploy/deploy 9 | :exec-args {:installer :local 10 | :artifact "target/cloel.jar"}}}} 11 | -------------------------------------------------------------------------------- /demo/app.el: -------------------------------------------------------------------------------- 1 | (require 'cloel) 2 | 3 | (defvar cloel-demo-dir (file-name-directory load-file-name)) 4 | 5 | (cloel-register-app "demo" cloel-demo-dir "cloel") 6 | 7 | (defun cloel-demo-test () 8 | (interactive) 9 | ;; STEP 1: Start Clojure process with localhost and free port. 10 | (cloel-demo-start-process)) 11 | 12 | (defun cloel-demo-start-process-confirm (client-id) 13 | ;; STEP 3: Send "Hello" message to Clojure process ASYNC. 14 | (message "Start process confirm: %s" client-id) 15 | (cloel-demo-send-message "Hello")) 16 | 17 | (defun cloel-demo-hello-confirm () 18 | ;; STEP 8: Call Clojure method "clojure.core/+" SYNC. 19 | (message "Got sync result of (+ 1 2 3): %s" (cloel-demo-call-sync "clojure.core/+" 1 2 3)) 20 | ;; STEP 9: Call Clojure method "app-success" ASYNC. 21 | (cloel-demo-call-async 'app/app-success "Cloel rocks!")) 22 | -------------------------------------------------------------------------------- /src/cloel/repl.clj: -------------------------------------------------------------------------------- 1 | (ns cloel.repl 2 | (:require 3 | cloel 4 | [clojure.java.io :as io]) 5 | (:import [java.net ServerSocket])) 6 | 7 | (def port-file-name ".cloel-port") 8 | 9 | (defn- random-available-port [] 10 | (with-open [socket (ServerSocket. 0)] 11 | (.getLocalPort socket))) 12 | 13 | (def server (atom nil)) 14 | 15 | (defn- delete-port-file [] 16 | (let [file (io/file port-file-name)] 17 | (when (.exists file) 18 | (io/delete-file file)))) 19 | 20 | (defn stop-server [] 21 | (when-let [s @server] 22 | (.close s) 23 | (reset! server nil)) 24 | (delete-port-file)) 25 | 26 | (defn start-server [] 27 | (stop-server) 28 | (let [port (random-available-port)] 29 | (spit port-file-name port) 30 | (reset! server (cloel/start-server port)))) 31 | 32 | (comment 33 | (start-server) 34 | (stop-server)) 35 | -------------------------------------------------------------------------------- /demo-bb/app.el: -------------------------------------------------------------------------------- 1 | (require 'cloel) 2 | 3 | (defvar cloel-demo-bb-dir 4 | (file-name-directory (or load-file-name (buffer-file-name)))) 5 | 6 | (cloel-register-app "demo-bb" cloel-demo-bb-dir "cloel" 'bb) 7 | 8 | (defun cloel-demo-bb-test () 9 | (interactive) 10 | ;; STEP 1: Start Clojure process with localhost and free port. 11 | (cloel-demo-bb-start-process)) 12 | 13 | (defun cloel-demo-bb-start-process-confirm (client-id) 14 | ;; STEP 3: Send "Hello" message to Clojure process ASYNC. 15 | (message "Start process confirm: %s" client-id) 16 | (cloel-demo-bb-send-message "Hello")) 17 | 18 | (defun cloel-demo-bb-hello-confirm () 19 | ;; STEP 8: Call Clojure method "clojure.core/+" SYNC. 20 | (message "Got sync result of (+ 1 2 3): %s" (cloel-demo-bb-call-sync "clojure.core/+" 1 2 3)) 21 | ;; STEP 9: Call Clojure method "app-success" ASYNC. 22 | (cloel-demo-bb-call-async 'app/app-success "Cloel rocks!")) 23 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | io.github.manateelazycat.cloel 5 | cloel 6 | 1.0.0 7 | cloel 8 | Cloel is a framework that combines Clojure and Elisp for collaborative programming 9 | 10 | 11 | org.clojure 12 | clojure 13 | 1.11.1 14 | 15 | 16 | 17 | src 18 | 19 | 20 | 21 | clojars 22 | https://repo.clojars.org/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/src/app.clj: -------------------------------------------------------------------------------- 1 | (ns app 2 | (:require [cloel :as cloel])) 3 | 4 | (defn app-start-process-confirm [data] 5 | ;; STEP 2: Clojure process started, call Elisp method 'cloel-demo-start-process-confirm' ASYNC. 6 | (cloel/elisp-eval-async "cloel-demo-start-process-confirm" (str data))) 7 | 8 | (defn app-handle-client-connected [client-id] 9 | (app-start-process-confirm client-id)) 10 | 11 | (defn app-handle-client-message [data] 12 | (when (= data "Hello") 13 | ;; STEP 4: Handle Emacs message in Clojure sub-thread. 14 | (future 15 | ;; STEP 5: Call Elisp method result SYNC. 16 | (println "Sync Result of (+ 2 3):" (cloel/elisp-eval-sync "+" 2 3)) 17 | ;; STEP 6: Get Elisp variable SYNC. 18 | (println "Value of 'user-full-name' is:" (cloel/elisp-get-var "user-full-name")) 19 | ;; STEP 7: Call Elisp method "cloel-demo-hello-confirm" ASYNC. 20 | (cloel/elisp-eval-async "cloel-demo-hello-confirm")))) 21 | 22 | (defn app-success [message] 23 | ;; STEP 10: Send message to Emacs ASYNC. 24 | (cloel/elisp-show-message message)) 25 | 26 | (alter-var-root #'cloel/handle-client-message (constantly app-handle-client-message)) 27 | (alter-var-root #'cloel/handle-client-connected (constantly app-handle-client-connected)) 28 | 29 | (defn -main [& args] 30 | (cloel/start-server (Integer/parseInt (last args)))) 31 | -------------------------------------------------------------------------------- /demo-bb/src/app.clj: -------------------------------------------------------------------------------- 1 | (ns app 2 | (:require [cloel :as cloel])) 3 | 4 | (defn app-start-process-confirm [data] 5 | ;; STEP 2: Clojure process started, call Elisp method 'cloel-demo-bb-start-process-confirm' ASYNC. 6 | (cloel/elisp-eval-async "cloel-demo-bb-start-process-confirm" (str data))) 7 | 8 | (defn app-handle-client-connected [client-id] 9 | (app-start-process-confirm client-id)) 10 | 11 | (defn app-handle-client-message [data] 12 | (when (= data "Hello") 13 | ;; STEP 4: Handle Emacs message in Clojure sub-thread. 14 | (future 15 | ;; STEP 5: Call Elisp method result SYNC. 16 | (println "Sync Result of (+ 2 3):" (cloel/elisp-eval-sync "+" 2 3)) 17 | ;; STEP 6: Get Elisp variable SYNC. 18 | (println "Value of 'user-full-name' is:" (cloel/elisp-get-var "user-full-name")) 19 | ;; STEP 7: Call Elisp method "cloel-demo-bb-hello-confirm" ASYNC. 20 | (cloel/elisp-eval-async "cloel-demo-bb-hello-confirm")))) 21 | 22 | (defn app-success [message] 23 | ;; STEP 10: Send message to Emacs ASYNC. 24 | (cloel/elisp-show-message message)) 25 | 26 | (alter-var-root #'cloel/handle-client-message (constantly app-handle-client-message)) 27 | (alter-var-root #'cloel/handle-client-connected (constantly app-handle-client-connected)) 28 | 29 | (defn -main [] 30 | (cloel/start-server (Integer/parseInt (last *command-line-args*))) 31 | (.addShutdownHook (Runtime/getRuntime) 32 | (Thread. #(println "Shutting down..."))) 33 | @(promise)) 34 | -------------------------------------------------------------------------------- /new: -------------------------------------------------------------------------------- 1 | (defun cloel-start-process (app-name) 2 | "Start the Clojure server process for APP-NAME." 3 | (let* ((app-data (cloel-get-app-data app-name)) 4 | (app-dir (plist-get app-data :dir)) 5 | (clj-type (plist-get app-data :type)) 6 | (app-aliases (or (plist-get app-data :aliases-or-bb-task) "cloel")) 7 | (port (cloel-get-free-port)) 8 | ;; 查找以 app-name 开头并以 .clj 结尾的文件 9 | (main-file (cl-find-if (lambda (file) 10 | (and (string-prefix-p app-name (file-name-nondirectory file)) 11 | (string-suffix-p ".clj" (file-name-nondirectory file)))) 12 | (directory-files app-dir t "\\.clj$")))) ;; 列出所有 .clj 文件 13 | 14 | (if main-file 15 | (let ((process (start-process (format "cloel-%s-clojure-server" app-name) 16 | (format "*cloel-%s-clojure-server*" app-name) 17 | "sh" 18 | "-c" 19 | (format "clojure -M %s %s" app-aliases main-file)))) ;; 加载找到的主文件 20 | (cloel-set-app-data app-name :server-process process) 21 | (set-process-sentinel process 22 | (lambda (proc event) 23 | (cloel-server-process-sentinel proc event app-name))) 24 | (message "Starting Clojure server for %s on port %d" app-name port) 25 | (cloel-connect-with-retry app-name "localhost" port)) 26 | (error "No main file found for %s" app-name)))) ;; 如果没有找到主文件则报错 27 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 简体中文 2 | 3 | # Cloel 4 | 5 | Cloel 是一个结合 Clojure 和 Elisp 的协同编程框架, 利用 Clojure 的生态和多线程能力来扩展 Emacs。 6 | 7 | 主要优势: 8 | 1. 速度快: 耗时代码在外部 Clojure 进程执行, 避免卡住 Emacs 9 | 2. 多线程: 利用 Clojure 的多线程, 保证快速响应 10 | 3. Lisp 风格: 用 Clojure 写插件也能保持 Lisp 风格 11 | 4. 强大生态: 可访问 JVM 和 Python 的软件包生态 12 | 5. 低门槛: 开箱即用的框架, 简化 Emacs 的 Clojure 扩展开发 13 | 14 | ## 安装 15 | 16 | 1. 安装 Clojure, 参考 [官方手册](https://clojure.org/guides/install_clojure) 17 | 18 | 2. 安装 [parseedn](https://github.com/clojure-emacs/parseedn) 19 | 20 | 3. 安装 Cloel Elisp 部分: 21 | - 克隆仓库并添加到 Emacs 配置: 22 | ```elisp 23 | (add-to-list 'load-path "") 24 | (require 'cloel) 25 | ``` 26 | 27 | 4. 安装 Cloel Clojure 部分: 28 | ```bash 29 | cd cloel 30 | clojure -X:jar 31 | clojure -X:install 32 | ``` 33 | 34 | - 中国用户, 可能会面临无法访问镜像源无法访问的问题, 请使用下面命令行来安装配置 35 | 36 | ```bash 37 | clojure -Sdeps '{:mvn/repos {"central" {:url "https://maven.aliyun.com/repository/public"} "clojars" {:url "https://mirrors.tuna.tsinghua.edu.cn/clojars"}}}' -X:jar 38 | clojure -Sdeps '{:mvn/repos {"central" {:url "https://maven.aliyun.com/repository/public"} "clojars" {:url "https://mirrors.tuna.tsinghua.edu.cn/clojars"}}}' -X:install 39 | ``` 40 | 41 | 5. 测试: 42 | - 执行 `M-x load-file` 选择 `cloel/demo/app.el` 43 | - 执行 `M-x cloel-demo-test` 44 | - 如果显示 "Cloel rocks!" 则安装成功, 没有成功请查看 `*cloel-demo-clojure-server*` buffer 以反馈报错信息 45 | 46 | ## 版本更新 47 | 每次更新 Cloel 后都需要执行 `clojure -X:jar; clojure -X:install` 来更新 Clojure 代码, 避免版本升级后的兼容性问题。 48 | 49 | ## 开发 50 | 开发时可通过 grep "STEP" 关键字来理解 Demo 程序结构。 51 | 52 | 以下是 API 详情, 将 `app` 替换为你的应用名: 53 | 54 | Elisp API: 55 | - `cloel-register-app`: 注册应用, 需要输入应用名, 应用目录, 别名以及 clj-type, 别名可选 `clojure` 或 `bb` 56 | - `cloel-app-start/stop/restart-process`: 管理 Clojure 进程 57 | - `cloel-app-send-message`: Elisp 异步发送消息给 Clojure 58 | - `cloel-app-call-async`: Elisp 异步调用 Clojure 函数 59 | - `cloel-app-call-sync`: Elisp 同步调用 Clojure 函数 60 | 61 | Clojure API: 62 | - `cloel/start-server`: 启动 Clojure 进程 63 | - `cloel/elisp-show-message`: Clojure 异步在 minibuffer 显示消息 64 | - `cloel/elisp-get-var`: Clojure 同步获取 Elisp 变量值 65 | - `cloel/elisp-eval-async`: Clojure 异步调用 Elisp 函数 66 | - `cloel/elisp-eval-sync`: Clojure 同步调用 Elisp 函数 67 | 68 | Clojure 可以被重载的接口: 69 | - `handle-client-async-call`: 处理 Elisp 异步调用 Clojure 函数的接口 70 | - `handle-client-sync-call`: 处理 Elisp 同步调用 Clojure 函数的接口 71 | - `handle-client-message`: 处理 Elisp 异步发送消息给 Clojure 的接口 72 | - `handle-client-connected`: Client 连接上 Clojure 进程时的接口 73 | 74 | 在 Clojure 中, 我们可以使用 `alter-var-root` 在应用端重载上面的接口实现, 具体请看 `demo/app.clj` 75 | 76 | ## 生态应用 77 | - [reorder-file](https://github.com/manateelazycat/reorder-file): 自动排序文件中的序号 78 | - [cloel-rand-quote](https://github.com/kimim/cloel-rand-quote): 插入一句格言警句(英语) 79 | -------------------------------------------------------------------------------- /cloel_test.el: -------------------------------------------------------------------------------- 1 | (require 'ert) 2 | 3 | (ert-deftest cloel-find-main-file-test () 4 | "Test finding main files in different scenarios." 5 | (let ((temp-dir (make-temp-file "cloel-test-" t))) 6 | (unwind-protect 7 | (progn 8 | ;; Test with file in src/ 9 | (make-directory (expand-file-name "src" temp-dir)) 10 | (with-temp-file (expand-file-name "src/test-app.clj" temp-dir) 11 | (insert "(ns test-app)\n(defn -main [& args])")) 12 | (should (string= (file-name-nondirectory (cloel-find-main-file "test-app" temp-dir)) 13 | "test-app.clj")) 14 | 15 | ;; Test with file in app-dir 16 | (delete-directory (expand-file-name "src" temp-dir) t) 17 | (with-temp-file (expand-file-name "test-app.clj" temp-dir) 18 | (insert "(ns test-app)\n(defn -main [& args])")) 19 | (should (string= (file-name-nondirectory (cloel-find-main-file "test-app" temp-dir)) 20 | "test-app.clj"))) 21 | (delete-directory temp-dir t)))) 22 | 23 | (ert-deftest cloel-determine-app-type-test () 24 | "Test determination of app types." 25 | (let ((temp-dir (make-temp-file "cloel-test-" t))) 26 | (unwind-protect 27 | (progn 28 | ;; Test deps.edn app 29 | (with-temp-file (expand-file-name "deps.edn" temp-dir) 30 | (insert "{:deps {}}")) 31 | (should (eq (cloel-determine-app-type temp-dir) 'deps)) 32 | 33 | ;; Test bb.edn app 34 | (delete-file (expand-file-name "deps.edn" temp-dir)) 35 | (with-temp-file (expand-file-name "bb.edn" temp-dir) 36 | (insert "{:tasks {}}")) 37 | (should (eq (cloel-determine-app-type temp-dir) 'bb)) 38 | 39 | ;; Test clojure-directory app 40 | (delete-file (expand-file-name "bb.edn" temp-dir)) 41 | (with-temp-file (expand-file-name "sample.clj" temp-dir) 42 | (insert "(ns sample)")) 43 | (should (eq (cloel-determine-app-type temp-dir) 'clojure-directory))) 44 | (delete-directory temp-dir t)))) 45 | 46 | (ert-deftest cloel-register-app-test () 47 | "Test registration of apps." 48 | (let ((cloel-apps (make-hash-table :test 'equal)) 49 | (temp-dir (make-temp-file "cloel-test-" t))) 50 | (unwind-protect 51 | (progn 52 | ;; Create a deps.edn file 53 | (with-temp-file (expand-file-name "deps.edn" temp-dir) 54 | (insert "{:deps {}}")) 55 | 56 | ;; Register the app 57 | (cloel-register-app "test-app" temp-dir) 58 | 59 | ;; Check it was registered properly 60 | (let ((app-data (cloel-get-app-data "test-app"))) 61 | (should app-data) 62 | (should (eq (plist-get app-data :type) 'deps)) 63 | (should (string= (plist-get app-data :dir) temp-dir)))) 64 | (delete-directory temp-dir t)))) 65 | 66 | (ert-deftest cloel-set-app-data-test () 67 | "Test setting app data." 68 | (let ((cloel-apps (make-hash-table :test 'equal))) 69 | ;; Register a dummy app 70 | (puthash "test-app" '(:dir "/tmp" :type deps) cloel-apps) 71 | 72 | ;; Set some data 73 | (cloel-set-app-data "test-app" :port 8080) 74 | 75 | ;; Check it was set correctly 76 | (should (= (plist-get (cloel-get-app-data "test-app") :port) 8080)))) 77 | 78 | (ert-deftest cloel-get-free-port-test () 79 | "Test getting a free port." 80 | (let ((port (cloel-get-free-port))) 81 | (should (numberp port)) 82 | (should (> port 1024)))) 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README.zh-CN.md) 2 | 3 | # Cloel 4 | 5 | Cloel is a collaborative programming framework that combines Clojure and Elisp, leveraging Clojure's ecosystem and multi-threading capabilities to extend Emacs. 6 | 7 | Key advantages: 8 | 1. Speed: Time-consuming code runs in an external Clojure process, avoiding Emacs freezes 9 | 2. Multi-threading: Utilizes Clojure's multi-threading to ensure fast responsiveness 10 | 3. Lisp style: Write plugins in Clojure while maintaining a Lisp style 11 | 4. Powerful ecosystem: Access to JVM and Python software package ecosystems 12 | 5. Low barrier: Out-of-the-box framework, simplifying Clojure extension development for Emacs 13 | 14 | ## Installation 15 | 16 | 1. Install Clojure, refer to the [official guide](https://clojure.org/guides/install_clojure) 17 | 18 | 2. Install [parseedn](https://github.com/clojure-emacs/parseedn) 19 | 20 | 3. Install Cloel Elisp part: 21 | - Clone the repository and add to your Emacs configuration: 22 | ```elisp 23 | (add-to-list 'load-path "") 24 | (require 'cloel) 25 | ``` 26 | 27 | 4. Install Cloel Clojure part: 28 | ```bash 29 | cd cloel 30 | clojure -X:jar 31 | clojure -X:install 32 | ``` 33 | 34 | 5. Test: 35 | - Execute `M-x load-file` and select `cloel/demo/app.el` 36 | - Run `M-x cloel-demo-test` 37 | - If "Cloel rocks!" is displayed, installation is successful. If not, check the `*cloel-demo-clojure-server*` buffer for error messages 38 | 39 | ## Version Update 40 | After each update of Cloel, you need to execute `clojure -X:jar; clojure -X:install` to update the Clojure code. 41 | 42 | ## Development 43 | During development, you can use the grep "STEP" keyword to understand the structure of the Demo program. 44 | 45 | Below are the API details, replace `app` with your application name: 46 | 47 | Elisp API: 48 | - `cloel-register-app`: Register an application. You need to input the application name, application directory, alias, and clj-type. The alias can be either `clojure` or `bb`. 49 | - `cloel-app-start/stop/restart-process`: Manage the Clojure process 50 | - `cloel-app-send-message`: Elisp asynchronously sends messages to Clojure 51 | - `cloel-app-call-async`: Elisp asynchronously calls Clojure functions 52 | - `cloel-app-call-sync`: Elisp synchronously calls Clojure functions 53 | 54 | Clojure API: 55 | - `cloel/start-server`: Start the Clojure process 56 | - `cloel/elisp-show-message`: Clojure asynchronously displays messages in the minibuffer 57 | - `cloel/elisp-get-var`: Clojure synchronously gets Elisp variable values 58 | - `cloel/elisp-eval-async`: Clojure asynchronously calls Elisp functions 59 | - `cloel/elisp-eval-sync`: Clojure synchronously calls Elisp functions 60 | 61 | Interfaces that can be reloaded in Clojure: 62 | - `handle-client-async-call`: Interface for handling Elisp asynchronous calls to Clojure functions 63 | - `handle-client-sync-call`: Interface for handling Elisp synchronous calls to Clojure functions 64 | - `handle-client-message`: Interface for handling Elisp asynchronously sending messages to Clojure 65 | - `handle-client-connected`: Interface for when the Client connects to the Clojure process 66 | 67 | In Clojure, we can use `alter-var-root` to reload the above interface implementations on the application side. For specifics, please refer to `demo/app.clj` 68 | 69 | ## Ecosystem Applications 70 | - [reorder-file](https://github.com/manateelazycat/reorder-file): Automatically reorders numbering within files 71 | - [cloel-rand-quote](https://github.com/kimim/cloel-rand-quote): Insert a random quote 72 | -------------------------------------------------------------------------------- /src/cloel.clj: -------------------------------------------------------------------------------- 1 | (ns cloel 2 | (:require [clojure.edn :as edn]) 3 | (:import [java.io BufferedReader InputStreamReader PrintWriter] 4 | [java.net ServerSocket Socket])) 5 | 6 | (def client-connection (atom nil)) 7 | (def call-results (atom nil)) 8 | (def call-id (atom 0)) 9 | 10 | (defn send-to-client [message] 11 | (when-let [writer (:writer @client-connection)] 12 | (.println writer (pr-str message)) 13 | (.flush writer))) 14 | 15 | (defn generate-call-id [] 16 | (swap! call-id inc)) 17 | 18 | (defn ^:export elisp-call [method & args] 19 | (let [id (generate-call-id) 20 | promise (promise)] 21 | (swap! call-results assoc id promise) 22 | (send-to-client {:type :call-elisp-sync :id id :method method :args args}) 23 | (let [result (deref promise 60000 :timeout)] 24 | (swap! call-results dissoc id) 25 | (if (= result :timeout) 26 | (throw (Exception. (str "Timeout waiting for Elisp response for id: " id))) 27 | result)))) 28 | 29 | (defn ^:export elisp-eval-sync [func & args] 30 | (elisp-call :get-func-result func args)) 31 | 32 | (defn ^:export elisp-eval-async [func & args] 33 | (send-to-client {:type :call-elisp-async :func func :args args})) 34 | 35 | (defn ^:export elisp-show-message [& args] 36 | (let [message (apply str args)] 37 | (elisp-eval-async "message" message))) 38 | 39 | (defn ^:export elisp-get-var [var-name] 40 | (elisp-call :get-var-result var-name)) 41 | 42 | (defn ^:dynamic handle-client-async-call [data] 43 | (future 44 | (let [{:keys [func args]} data] 45 | (try 46 | (apply (resolve (symbol func)) args) 47 | (catch Exception e 48 | (println "Error in Clojure call:" (.getMessage e))))))) 49 | 50 | (defn ^:dynamic handle-client-sync-call [data] 51 | (future 52 | (let [{:keys [id func args]} data 53 | result (try 54 | {:value (apply (resolve (symbol func)) args)} 55 | (catch Exception e 56 | {:error (.getMessage e)}))] 57 | (send-to-client {:type :clojure-sync-return 58 | :id id 59 | :result result})))) 60 | 61 | (defn ^:dynamic handle-client-message [data] 62 | (println "Received message:" data)) 63 | 64 | (defn ^:dynamic handle-client-connected [client-id] 65 | (println "Client connected:" client-id)) 66 | 67 | (defn handle-client [^Socket client-socket] 68 | (let [client-id (.toString (.getRemoteSocketAddress client-socket)) 69 | reader (BufferedReader. (InputStreamReader. (.getInputStream client-socket))) 70 | writer (PrintWriter. (.getOutputStream client-socket))] 71 | (reset! client-connection {:socket client-socket :reader reader :writer writer}) 72 | (handle-client-connected client-id) 73 | (try 74 | (loop [] 75 | (when-let [input (.readLine reader)] 76 | (let [data (edn/read-string input)] 77 | (println "Received from client:" data) 78 | (cond 79 | (and (map? data) (= (:type data) :elisp-sync-return)) 80 | (when-let [promise (get @call-results (:id data))] 81 | (deliver promise (:value data))) 82 | 83 | (and (map? data) (= (:type data) :call-clojure-async)) 84 | (handle-client-async-call data) 85 | 86 | (and (map? data) (= (:type data) :call-clojure-sync)) 87 | (handle-client-sync-call data) 88 | 89 | :else 90 | (handle-client-message data)) 91 | (recur)))) 92 | (catch Exception e 93 | (println "Client disconnected:" (.getMessage e))) 94 | (finally 95 | (reset! client-connection nil) 96 | (.close client-socket))))) 97 | 98 | (defn ^:export start-server [port] 99 | (let [server-socket (ServerSocket. port)] 100 | (println "Server started on port" port) 101 | (future 102 | (while true 103 | (when (nil? @client-connection) 104 | (let [client-socket (.accept server-socket)] 105 | (future (handle-client client-socket)))))) 106 | (println "Waiting for client connection...") 107 | server-socket)) 108 | -------------------------------------------------------------------------------- /cloel.el: -------------------------------------------------------------------------------- 1 | ;;; cloel.el --- Cloel -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: cloel.el 4 | ;; Description: Cloel 5 | ;; Author: Andy Stewart 6 | ;; Maintainer: Andy Stewart 7 | ;; Copyright (C) 2024, Andy Stewart, all rights reserved. 8 | ;; Created: 2024-09-19 23:08:26 9 | ;; Version: 0.1 10 | ;; Last-Updated: 2024-09-19 23:08:26 11 | ;; By: Andy Stewart 12 | ;; URL: https://www.github.org/manateelazycat/cloel 13 | ;; Keywords: 14 | ;; Compatibility: GNU Emacs 31.0.50 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Commentary: 41 | ;; 42 | ;; Cloel 43 | ;; 44 | 45 | ;;; Installation: 46 | ;; 47 | ;; Put cloel.el to your load-path. 48 | ;; The load-path is usually ~/elisp/. 49 | ;; It's set in your ~/.emacs like this: 50 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 51 | ;; 52 | ;; And the following to your ~/.emacs startup file. 53 | ;; 54 | ;; (require 'cloel) 55 | ;; 56 | ;; No need more. 57 | 58 | ;;; Customize: 59 | ;; 60 | ;; 61 | ;; 62 | ;; All of the above can customize by: 63 | ;; M-x customize-group RET cloel RET 64 | ;; 65 | 66 | ;;; Change log: 67 | ;; 68 | ;; 2024/09/19 69 | ;; * First released. 70 | ;; 71 | 72 | ;;; Acknowledgements: 73 | ;; 74 | ;; 75 | ;; 76 | 77 | ;;; TODO 78 | ;; 79 | ;; 80 | ;; 81 | 82 | ;;; Require 83 | (require 'cl-lib) 84 | (require 'parseedn) 85 | 86 | ;;; Code: 87 | (defvar cloel-apps (make-hash-table :test 'equal) 88 | "Hash table to store app-specific data.") 89 | 90 | (defvar cloel-max-retries 5 91 | "Maximum number of connection retries.") 92 | 93 | (defvar cloel-retry-delay 1 94 | "Delay in seconds between connection retries.") 95 | 96 | (defvar cloel-sync-call-results (make-hash-table :test 'equal) 97 | "Hash table to store sync call results.") 98 | 99 | 100 | (defgroup cloel nil 101 | "Clojure Bridge for Emacs." 102 | :group 'applications) 103 | 104 | (defcustom cloel-max-retries 5 105 | "Maximum number of connection retries." 106 | :type 'integer 107 | :group 'cloel) 108 | 109 | (defcustom cloel-retry-delay 1 110 | "Delay in seconds between connection retries." 111 | :type 'number 112 | :group 'cloel) 113 | 114 | (defcustom cloel-sync-timeout 60 115 | "Timeout in seconds for synchronous calls." 116 | :type 'number 117 | :group 'cloel) 118 | 119 | (defcustom cloel-default-port-file ".cloel-port" 120 | "Default filename for port discovery." 121 | :type 'string 122 | :group 'cloel) 123 | 124 | 125 | (defun cloel-log-error (format-string &rest args) 126 | "Log an error message using FORMAT-STRING and ARGS." 127 | (let ((message (apply #'format format-string args))) 128 | (message "Cloel Error: %s" message) 129 | message)) 130 | 131 | 132 | (defun cloel-ensure-process-killed (process) 133 | "Ensure PROCESS is killed safely." 134 | (when (and process (process-live-p process)) 135 | (let ((pid (process-id process))) 136 | (delete-process process) 137 | (when pid 138 | (ignore-errors 139 | (signal-process pid 'SIGTERM) 140 | (sleep-for 0.5) 141 | (when (process-running-p pid) 142 | (signal-process pid 'SIGKILL))))))) 143 | 144 | 145 | (defun cloel-generate-app-functions (app-name) 146 | "Generate app-specific functions for APP-NAME." 147 | (let ((start-func-name (intern (format "cloel-%s-start-process" app-name))) 148 | (stop-func-name (intern (format "cloel-%s-stop-process" app-name))) 149 | (restart-func-name (intern (format "cloel-%s-restart-process" app-name))) 150 | (call-async-func-name (intern (format "cloel-%s-call-async" app-name))) 151 | (call-sync-func-name (intern (format "cloel-%s-call-sync" app-name))) 152 | (send-message-func-name (intern (format "cloel-%s-send-message" app-name)))) 153 | 154 | (fset start-func-name 155 | `(lambda () 156 | (interactive) 157 | (cloel-start-process ',app-name))) 158 | 159 | (fset stop-func-name 160 | `(lambda () 161 | (interactive) 162 | (cloel-stop-process ',app-name))) 163 | 164 | (fset restart-func-name 165 | `(lambda () 166 | (interactive) 167 | (cloel-restart-process ',app-name))) 168 | 169 | (fset call-async-func-name 170 | `(lambda (func &rest args) 171 | (apply 'cloel-call-async ',app-name func args))) 172 | 173 | (fset call-sync-func-name 174 | `(lambda (func &rest args) 175 | (apply 'cloel-call-sync ',app-name func args))) 176 | 177 | (fset send-message-func-name 178 | `(lambda (message) 179 | (cloel-send-message ',app-name message))) 180 | 181 | (list start-func-name stop-func-name restart-func-name call-async-func-name call-sync-func-name send-message-func-name))) 182 | 183 | (defun cloel-register-app (app-name app-dir &optional aliases-or-bb-task clj-type) 184 | "Register an app with APP-NAME and APP-DIR." 185 | (let* ((app-dir (expand-file-name app-dir)) 186 | (inferred-clj-type (or clj-type (cloel-determine-app-type app-dir)))) 187 | (unless inferred-clj-type 188 | (error "Cannot determine app type for directory: %s. Please provide clj-type or ensure deps.edn/bb.edn exists." app-dir)) 189 | (puthash app-name 190 | (list :dir app-dir 191 | :type inferred-clj-type 192 | :port-from-file (cloel-get-free-port-from-port-file) 193 | :aliases-or-bb-task (or aliases-or-bb-task 'clojure) 194 | :server-process nil 195 | :tcp-channel nil) 196 | cloel-apps) 197 | (cloel-generate-app-functions app-name))) 198 | 199 | (defun cloel-get-app-data (app-name) 200 | "Get the data for APP-NAME." 201 | (gethash app-name cloel-apps)) 202 | 203 | (defun cloel-set-app-data (app-name key value) 204 | "Set KEY to VALUE for APP-NAME." 205 | (when-let ((app-data (cloel-get-app-data app-name))) 206 | (puthash app-name (plist-put app-data key value) cloel-apps))) 207 | 208 | (defun cloel-get-free-port-from-port-file () 209 | "Get port specified in .cloel-port file." 210 | (let ((port-file (expand-file-name ".cloel-port" default-directory))) 211 | (when (file-exists-p port-file) 212 | (with-temp-buffer 213 | (insert-file-contents port-file) 214 | (string-to-number (buffer-string)))))) 215 | 216 | ;; (defun cloel-get-free-port () 217 | ;; "Find a free port." 218 | ;; (let ((process (make-network-process :name "cloel-port-finder" 219 | ;; :service t 220 | ;; :host 'local 221 | ;; :server t))) 222 | ;; (prog1 (process-contact process :service) 223 | ;; (delete-process process)))) 224 | 225 | (defun cloel-get-free-port () 226 | "Find a free port by binding to it and ensuring it remains available." 227 | (let (port process) 228 | (while (not port) 229 | ;; try to bind random port number 230 | (setq process (make-network-process :name "cloel-port-finder" 231 | :service t 232 | :host 'local 233 | :server t)) 234 | (setq port (process-contact process :service)) 235 | ;; check if port available 236 | (condition-case nil 237 | (progn 238 | (delete-process process) 239 | (setq process (make-network-process :name "cloel-port-checker" 240 | :service port 241 | :host 'local 242 | :server t)) 243 | (delete-process process) 244 | (message "Found free port: %d" port)) 245 | (error 246 | (setq port nil) ;; if port not useful, try another port 247 | (when process (delete-process process))))) 248 | port)) 249 | 250 | (defun cloel-determine-app-type (app-dir) 251 | "Determine the type of the Clojure app based on its directory structure." 252 | (cond 253 | ((file-exists-p (expand-file-name "deps.edn" app-dir)) 'deps) 254 | ((file-exists-p (expand-file-name "bb.edn" app-dir)) 'bb) 255 | ((and (file-directory-p app-dir) (directory-files app-dir t ".*\.clj")) 'clojure-directory) 256 | ((and (file-regular-p app-dir) (string-suffix-p ".clj" app-dir)) 'clojure) 257 | (t (error "Cannot determine app type for directory: %s" app-dir)))) 258 | 259 | 260 | (defun cloel-find-main-file (app-name app-dir) 261 | "Find the main Clojure file for APP-NAME in APP-DIR or its src subdirectory." 262 | (let ((src-dir (expand-file-name "src" app-dir))) 263 | (cond 264 | ;; First look for a file named after the app in src/ 265 | ((and (file-directory-p src-dir) 266 | (file-exists-p (expand-file-name (format "%s.clj" app-name) src-dir))) 267 | (expand-file-name (format "%s.clj" app-name) src-dir)) 268 | 269 | ;; Then look for any .clj file in src/ 270 | ((and (file-directory-p src-dir) 271 | (directory-files src-dir t "\\.clj$")) 272 | (car (directory-files src-dir t "\\.clj$"))) 273 | 274 | ;; Look directly in app-dir for app-name.clj 275 | ((file-exists-p (expand-file-name (format "%s.clj" app-name) app-dir)) 276 | (expand-file-name (format "%s.clj" app-name) app-dir)) 277 | 278 | ;; Finally, look for any .clj file in app-dir 279 | ((directory-files app-dir t "\\.clj$") 280 | (car (directory-files app-dir t "\\.clj$"))) 281 | 282 | ;; Nothing found 283 | (t (error "Cannot find .clj file for app: %s in directory: %s or src/" app-name app-dir))))) 284 | 285 | 286 | (defun cloel-start-process (app-name) 287 | "Start the Clojure server process for APP-NAME." 288 | (let* ((app-data (cloel-get-app-data app-name)) 289 | (port (plist-get app-data :port-from-file)) 290 | (clj-type (plist-get app-data :type))) 291 | (if port 292 | (cloel-connect-with-retry app-name "localhost" port) 293 | (let* ((app-dir (plist-get app-data :dir)) 294 | (app-aliases (or (plist-get app-data :aliases-or-bb-task) "cloel")) 295 | (main-file (cloel-find-main-file app-name app-dir)) 296 | (default-directory app-dir) 297 | (app-deps-edn (expand-file-name "deps.edn" app-dir)) 298 | (app-bb-edn (expand-file-name "bb.edn" app-dir)) 299 | (port (cloel-get-free-port))) 300 | (when (and (eq clj-type 'clojure) (not (file-exists-p app-deps-edn))) 301 | (error "can not find app deps.edn at %s" app-deps-edn)) 302 | (when (and (eq clj-type 'deps) (not (file-exists-p app-deps-edn))) 303 | (error "can not find app deps.edn at %s" app-deps-edn)) 304 | (when (and (eq clj-type 'bb) (not (file-exists-p app-bb-edn))) 305 | (error "can not find app bb.edn at %s" app-bb-edn)) 306 | (let ((process (start-process (format "cloel-%s-clojure-server" app-name) 307 | (format "*cloel-%s-clojure-server*" app-name) 308 | "sh" 309 | "-c" 310 | (pcase clj-type 311 | ('clojure (format "clojure -M:%s %s %d" app-aliases main-file port)) 312 | ('deps (format "clojure -M:%s %s %d" app-aliases main-file port)) 313 | ('bb (format "bb %s %d" app-aliases port)) 314 | (_ (error "Unknown clj-type: %s" clj-type)))))) 315 | (message "Starting Clojure server with command: %s" 316 | (pcase clj-type 317 | ('clojure (format "clojure -M:%s %s %d" app-aliases main-file port)) 318 | ('deps (format "clojure -M:%s %s %d" app-aliases main-file port)) 319 | ('bb (format "bb %s %d" app-aliases port)))) 320 | (cloel-set-app-data app-name :server-process process) 321 | (message "Starting Clojure server for %s on port %d" app-name port) 322 | (set-process-sentinel process 323 | (lambda (proc event) 324 | (message "clojure server process for %s has stopped: %s" app-name event) 325 | (cloel-server-process-sentinel proc event app-name))) 326 | (sleep-for 3) ;; sleep 3s for some scenes 327 | (cloel-connect-with-retry app-name "localhost" port)))))) 328 | 329 | (defun cloel-stop-process (app-name) 330 | "Stop the Clojure server process and disconnect the client for APP-NAME." 331 | (let* ((app-data (cloel-get-app-data app-name)) 332 | (tcp-channel (plist-get app-data :tcp-channel)) 333 | (server-process (plist-get app-data :server-process))) 334 | (when (and tcp-channel (process-live-p tcp-channel)) 335 | (delete-process tcp-channel) 336 | (cloel-set-app-data app-name :tcp-channel nil) 337 | (message "Disconnected Clojure TCP connection for %s" app-name)) 338 | (when (and server-process (process-live-p server-process)) 339 | (delete-process server-process) 340 | (cloel-set-app-data app-name :server-process nil) 341 | (message "Stopped Clojure server process for %s" app-name)))) 342 | 343 | (defun cloel-restart-process (app-name) 344 | "Restart the Clojure server process for APP-NAME." 345 | (cloel-stop-process app-name) 346 | (sleep-for 1) 347 | (cloel-start-process app-name)) 348 | 349 | (defun cloel-server-process-sentinel (process event app-name) 350 | "Handle Clojure process state changes for APP-NAME." 351 | (when (memq (process-status process) '(exit signal)) 352 | (message "Clojure server process for %s has stopped: %s" app-name event) 353 | (cloel-set-app-data app-name :server-process nil))) 354 | 355 | (defun cloel-connect-with-retry (app-name host port) 356 | "Attempt to connect to the Clojure server with retries for APP-NAME." 357 | (let ((retries 0) 358 | (connected nil)) 359 | (while (and (not connected) (< retries cloel-max-retries)) 360 | (condition-case err 361 | (progn 362 | (cloel-connect app-name host port) 363 | (setq connected t)) 364 | (error 365 | (setq retries (1+ retries)) 366 | (when (< retries cloel-max-retries) 367 | (sleep-for cloel-retry-delay))))) 368 | (unless connected 369 | (error "Failed to connect after %d attempts for %s" cloel-max-retries app-name)))) 370 | 371 | (defun cloel-connect (app-name host port) 372 | "Establish a connection to a Clojure server at HOST:PORT for APP-NAME." 373 | (let* ((port-num (if (stringp port) (string-to-number port) port)) 374 | (channel (open-network-stream (format "cloel-%s-client" app-name) 375 | (format "*cloel-%s-client*" app-name) 376 | host port-num))) 377 | (cloel-set-app-data app-name :tcp-channel channel) 378 | 379 | ;; TODO: this is ugly 380 | ;; use channel as server-process when port file exists 381 | ;; so that process-live-p against :server-process works 382 | (let ((app-data (cloel-get-app-data app-name))) 383 | (when (and (plist-get app-data :port-from-file) 384 | (not (plist-get app-data :server-process))) 385 | (cloel-set-app-data app-name :server-process channel))) 386 | 387 | (set-process-coding-system channel 'utf-8 'utf-8) 388 | (if (process-live-p channel) 389 | (progn 390 | (set-process-filter channel 391 | (lambda (proc output) 392 | (cloel-tcp-connection-filter proc output app-name))) 393 | (set-process-sentinel channel 394 | (lambda (proc event) 395 | (cloel-tcp-connection-sentinel proc event app-name))) 396 | (message "TCP connected for %s at %s:%s" app-name host port-num)) 397 | (error "Failed to TCP connect for %s at %s:%s" app-name host port-num)))) 398 | 399 | (defun cloel-send-message (app-name message) 400 | "Send MESSAGE to the connected Clojure server for APP-NAME." 401 | (let ((channel (plist-get (cloel-get-app-data app-name) :tcp-channel))) 402 | (if (process-live-p channel) 403 | (let ((encoded-message 404 | (condition-case err 405 | (parseedn-print-str message) 406 | (error 407 | (message "Error encoding message: %S" err) 408 | (prin1-to-string message))))) 409 | (process-send-string channel (concat encoded-message "\n"))) 410 | (error "Not connected to Clojure server for %s" app-name)))) 411 | 412 | (defun cloel-tcp-connection-filter (proc output app-name) 413 | "Handle output from the Clojure server for APP-NAME." 414 | (with-current-buffer (process-buffer proc) 415 | (goto-char (point-max)) 416 | (insert output)) 417 | (when-let ((data (condition-case err 418 | (parseedn-read-str output) 419 | (error (message "Error parsing output for %s: %S" app-name err) nil)))) 420 | (if (and (hash-table-p data) (gethash :type data)) 421 | (cl-case (gethash :type data) 422 | (:call-elisp-sync (cloel-handle-sync-call proc data app-name)) 423 | (:call-elisp-async (cloel-handle-async-call data app-name)) 424 | (:clojure-sync-return (cloel-handle-sync-return data)) 425 | (t (message "Received unknown message type for %s: %s" app-name (gethash :type data))))))) 426 | 427 | (defun cloel-handle-sync-call (proc data app-name) 428 | "Handle a call from the Clojure server for APP-NAME." 429 | (let* ((id (gethash :id data)) 430 | (method (gethash :method data)) 431 | (args (gethash :args data)) 432 | result) 433 | (condition-case err 434 | (setq result 435 | (cond 436 | ((eq method :get-func-result) 437 | (if (and (stringp (car args)) (fboundp (intern (car args)))) 438 | (apply (intern (car args)) (car (cdr args))) 439 | (error "Invalid function or arguments"))) 440 | ((eq method :get-var-result) (symbol-value (intern (car args)))) 441 | (t (error "Unknown method: %s" method)))) 442 | (error (setq result (cons 'error (error-message-string err))))) 443 | (let ((response (make-hash-table :test 'equal))) 444 | (puthash :type :elisp-sync-return response) 445 | (puthash :id id response) 446 | (puthash :value (if (and (consp result) (eq (car result) 'error)) 447 | (format "%s" (cdr result)) 448 | result) 449 | response) 450 | (cloel-send-message app-name response)))) 451 | 452 | (defun cloel-handle-sync-return (data) 453 | "Handle synchronous call return from Clojure server." 454 | (let* ((call-id (gethash :id data)) 455 | (result (gethash :result data)) 456 | (result-promise (gethash call-id cloel-sync-call-results))) 457 | (when result-promise 458 | (puthash :result result result-promise) 459 | (remhash call-id cloel-sync-call-results)))) 460 | 461 | (defun cloel-handle-async-call (data app-name) 462 | "Handle asynchronous evaluation request from Clojure." 463 | (let ((func (gethash :func data)) 464 | (args (gethash :args data))) 465 | (condition-case err 466 | (if (and (stringp func) (fboundp (intern func))) 467 | (apply (intern func) args) 468 | (error "Invalid function or arguments")) 469 | (error (message "Error in async eval for %s: %s" app-name (error-message-string err)))))) 470 | 471 | (defun cloel-tcp-connection-sentinel (proc event app-name) 472 | "Monitor the network connection for APP-NAME." 473 | (when (string-match "\\(closed\\|connection broken by remote peer\\)" event) 474 | (message "TCP connection to server for %s was closed" app-name) 475 | (cloel-set-app-data app-name :tcp-channel nil))) 476 | 477 | (defun cloel-call-async (app-name func &rest args) 478 | "Call Clojure function FUNC with ARGS for APP-NAME." 479 | (cloel-send-message app-name 480 | (let ((message (make-hash-table :test 'equal))) 481 | (puthash :type :call-clojure-async message) 482 | (puthash :func func message) 483 | (puthash :args args message) 484 | message))) 485 | 486 | (defun cloel-call-sync (app-name func &rest args) 487 | "Synchronously call Clojure function FUNC with ARGS for APP-NAME and return the result." 488 | (let* ((call-id (format "%s-%s" app-name (cl-gensym))) 489 | (result-promise (make-hash-table :test 'equal)) 490 | (start-time (current-time)) 491 | result) 492 | (puthash call-id result-promise cloel-sync-call-results) 493 | (cloel-send-message app-name 494 | (let ((message (make-hash-table :test 'equal))) 495 | (puthash :type :call-clojure-sync message) 496 | (puthash :id call-id message) 497 | (puthash :func func message) 498 | (puthash :args args message) 499 | message)) 500 | (cl-loop with wait-time = 0.001 501 | for elapsed = (float-time (time-subtract (current-time) start-time)) 502 | do (setq result (gethash :result result-promise)) 503 | until result 504 | when (> elapsed 60) do (error "Timeout waiting for sync call result") 505 | do (sleep-for wait-time) 506 | do (setq wait-time (min (* wait-time 1.5) 0.1))) 507 | (remhash call-id cloel-sync-call-results) 508 | (let ((return-value 509 | (if (hash-table-p result) 510 | (if (gethash :error result) 511 | (error "Clojure error: %s" (gethash :error result)) 512 | (gethash :value result)) 513 | (error "Unexpected result format: %S" result)))) 514 | return-value))) 515 | 516 | (provide 'cloel) 517 | ;;; cloel.el ends here 518 | --------------------------------------------------------------------------------