├── 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 |
--------------------------------------------------------------------------------