From 649e33fe3b3af997fa4c84e82fc8104bf385d274 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Tue, 30 Jun 2026 14:33:26 -0400 Subject: [PATCH] Add :repl/quit and :exit gestures to the REPL ^D (EOF) exits cleanly in canonical mode but some terminals and editors don't deliver it, leaving the user stuck. Accepting :repl/quit or :exit as the first form of a line gives a reliable keyword exit that works everywhere. The check parses the line with read-string rather than checking the evaluated value, so a nested value that happens to print as the keyword can't trigger an exit. --- host/chez/smoke.sh | 21 +++++++++++++++++++++ jolt-core/jolt/main.clj | 19 ++++++++++++------- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/host/chez/smoke.sh b/host/chez/smoke.sh index 6cdc299..5059e6e 100755 --- a/host/chez/smoke.sh +++ b/host/chez/smoke.sh @@ -67,5 +67,26 @@ else fails=$((fails + 1)) fi +# REPL must exit on :repl/quit / :exit — a reliable exit that works in any +# terminal, unlike ^D (which some terminals/editors don't deliver as EOF). +# Pipe: an evaluable form, the quit keyword, then a sentinel that must NOT run. +repl_out="$(printf '(+ 1000 23)\n:repl/quit\n(* 999 9)\n' | bin/joltc repl 2>/dev/null)" +if printf '%s' "$repl_out" | grep -q '1023' && ! printf '%s' "$repl_out" | grep -q '8991'; then + pass=$((pass + 1)) +else + echo " FAIL: repl should exit on :repl/quit before later forms" + printf '%s\n' "$repl_out" | sed 's/^/ | /' + fails=$((fails + 1)) +fi + +repl_out="$(printf '(- 2024 1)\n:exit\n(* 999 9)\n' | bin/joltc repl 2>/dev/null)" +if printf '%s' "$repl_out" | grep -q '2023' && ! printf '%s' "$repl_out" | grep -q '8991'; then + pass=$((pass + 1)) +else + echo " FAIL: repl should exit on :exit before later forms" + printf '%s\n' "$repl_out" | sed 's/^/ | /' + fails=$((fails + 1)) +fi + echo "cli smoke: $pass passed, $fails failed" [ "$fails" -eq 0 ] diff --git a/jolt-core/jolt/main.clj b/jolt-core/jolt/main.clj index 392979c..b8b0d36 100644 --- a/jolt-core/jolt/main.clj +++ b/jolt-core/jolt/main.clj @@ -93,17 +93,22 @@ ;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL. (try (apply-project! (deps/resolve-project (project-dir))) (catch :default _ nil)) - (println ";; jolt repl — ^D to exit") + (println ";; jolt repl — :repl/quit or ^D to exit") (loop [] (print "user=> ") (flush) (let [line (read-line)] (when line - (try (println (pr-str (load-string line))) - (catch :default e - (println "error:" (or (ex-message e) - (try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil)) - (pr-str e))))) - (recur))))) + ;; :repl/quit / :exit exit the loop — a reliable gesture that works in any + ;; terminal, unlike ^D (some terminals/editors don't deliver it as EOF). + (if (#{:repl/quit :exit} (try (read-string line) (catch :default _ nil))) + nil + (do + (try (println (pr-str (load-string line))) + (catch :default e + (println "error:" (or (ex-message e) + (try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil)) + (pr-str e))))) + (recur))))))) ;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}. (defn- run-task [name more]