Switch embedded REPL from bare Chez to full Jerboa REPL with 40+ comma commands

ober

82372d9043f021151b8462031d5f48f993735b4d

diff --git a/lib/jerboa-emacs/qt/app.sls b/lib/jerboa-emacs/qt/app.sls
index 841e5c1..47dcca0 100644
--- a/lib/jerboa-emacs/qt/app.sls
+++ b/lib/jerboa-emacs/qt/app.sls
@@ -1531,9 +1531,6 @@
                                      (qt-plain-text-edit-append!
                                        ed
                                        trimmed)
-                                     (qt-plain-text-edit-append!
-                                       ed
-                                       repl-prompt)
                                      (repl-state-prompt-pos-set!
                                        rs
                                        (string-length
diff --git a/lib/jerboa-emacs/qt/commands-edit.sls b/lib/jerboa-emacs/qt/commands-edit.sls
index 6b6dbeb..47031bf 100644
--- a/lib/jerboa-emacs/qt/commands-edit.sls
+++ b/lib/jerboa-emacs/qt/commands-edit.sls
@@ -1251,11 +1251,8 @@
                (qt-edit-window-buffer-set! (qt-current-window fr) buf)
                (let ([rs (repl-start!)])
                  (hash-put! *repl-state* buf rs)
-                 (qt-plain-text-edit-set-text! ed repl-prompt)
-                 (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
-                 (repl-state-prompt-pos-set!
-                   rs
-                   (string-length repl-prompt)))
+                 (qt-plain-text-edit-set-text! ed "")
+                 (repl-state-prompt-pos-set! rs 999999999))
                (echo-message! (app-state-echo app) "REPL started")))))
   (def (cmd-repl-send app)
        "Send the current input line to the Chez Scheme subprocess."
diff --git a/src/jerboa-emacs/app.ss b/src/jerboa-emacs/app.ss
index ecdb35c..f1db72c 100644
--- a/src/jerboa-emacs/app.ss
+++ b/src/jerboa-emacs/app.ss
@@ -341,9 +341,8 @@
                 (let ((win (find-window-for-buffer (app-state-frame app) buf)))
                   (when win
                     (let ((ed (edit-window-editor win)))
-                      ;; Insert output + new prompt at end
+                      ;; Insert output (subprocess sends its own prompt)
                       (editor-append-text ed output)
-                      (editor-append-text ed repl-prompt)
                       ;; Update prompt-pos to after the new prompt
                       (set! (repl-state-prompt-pos rs)
                         (editor-get-text-length ed))
diff --git a/src/jerboa-emacs/editor-core.ss b/src/jerboa-emacs/editor-core.ss
index 9287627..96575c9 100644
--- a/src/jerboa-emacs/editor-core.ss
+++ b/src/jerboa-emacs/editor-core.ss
@@ -2199,14 +2199,13 @@
         ;; Attach buffer to editor
         (buffer-attach! ed buf)
         (set! (edit-window-buffer (current-window fr)) buf)
-        ;; Spawn Chez Scheme subprocess
+        ;; Spawn Jerboa REPL subprocess
         (let ((rs (repl-start!)))
           (hash-put! *repl-state* buf rs)
-          ;; Insert initial prompt
-          (editor-set-text ed repl-prompt)
-          (let ((len (editor-get-text-length ed)))
-            (set! (repl-state-prompt-pos rs) len)
-            (editor-goto-pos ed len)))
+          ;; Don't insert prompt — subprocess sends its own banner + prompt.
+          ;; Set prompt-pos high to block typing until first poll delivers output.
+          (editor-set-text ed "")
+          (set! (repl-state-prompt-pos rs) 999999999))
         (echo-message! (app-state-echo app) "REPL started")))))
 
 (def (cmd-repl-send app)
diff --git a/src/jerboa-emacs/qt/app.ss b/src/jerboa-emacs/qt/app.ss
index b9e6685..a5edacc 100644
--- a/src/jerboa-emacs/qt/app.ss
+++ b/src/jerboa-emacs/qt/app.ss
@@ -1234,9 +1234,8 @@
                               (let* ((ed (qt-edit-window-editor (car wins)))
                                      ;; Strip trailing newline — append! adds its own
                                      (trimmed (string-trim-eol output)))
-                                ;; Insert output + new prompt
+                                ;; Insert output (subprocess sends its own prompt)
                                 (qt-plain-text-edit-append! ed trimmed)
-                                (qt-plain-text-edit-append! ed repl-prompt)
                                 (set! (repl-state-prompt-pos rs)
                                   (string-length (qt-plain-text-edit-text ed)))
                                 (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
diff --git a/src/jerboa-emacs/qt/commands-edit.ss b/src/jerboa-emacs/qt/commands-edit.ss
index 05cb579..76264c5 100644
--- a/src/jerboa-emacs/qt/commands-edit.ss
+++ b/src/jerboa-emacs/qt/commands-edit.ss
@@ -1126,16 +1126,13 @@
         ;; Attach buffer to editor
         (qt-buffer-attach! ed buf)
         (set! (qt-edit-window-buffer (qt-current-window fr)) buf)
-        ;; Spawn Chez Scheme subprocess
+        ;; Spawn Jerboa REPL subprocess
         (let ((rs (repl-start!)))
           (hash-put! *repl-state* buf rs)
-          ;; Show prompt immediately — scheme -q in non-interactive mode
-          ;; does NOT send a startup banner, so the timer would never fire and
-          ;; prompt-pos would stay at 999999999, blocking all typing.
-          (qt-plain-text-edit-set-text! ed repl-prompt)
-          (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
-          (set! (repl-state-prompt-pos rs)
-            (string-length repl-prompt)))
+          ;; Don't insert prompt — the subprocess sends its own banner + prompt.
+          ;; Set prompt-pos high to block typing until first poll delivers output.
+          (qt-plain-text-edit-set-text! ed "")
+          (set! (repl-state-prompt-pos rs) 999999999))
         (echo-message! (app-state-echo app) "REPL started")))))
 
 (def (cmd-repl-send app)
diff --git a/src/jerboa-emacs/repl.ss b/src/jerboa-emacs/repl.ss
index 2efe3e0..bc3dd1f 100644
--- a/src/jerboa-emacs/repl.ss
+++ b/src/jerboa-emacs/repl.ss
@@ -1,7 +1,8 @@
 ;;; -*- Gerbil -*-
 ;;; REPL subprocess management for jemacs
 ;;;
-;;; Manages a Chez Scheme subprocess: spawn, send input, read output, stop.
+;;; Manages a Jerboa REPL subprocess: spawn, send input, read output, stop.
+;;; Uses (std repl) with 40+ comma commands, value history, etc.
 ;;; No backend imports — shared between TUI and Qt.
 
 (export
@@ -25,23 +26,107 @@
    history)      ; list of previous inputs
   transparent: #t)
 
-(def repl-prompt "chez> ")
+(def repl-prompt "jerboa> ")
 
 ;;;============================================================================
 ;;; Lifecycle
 ;;;============================================================================
 
+(def (jerboa-lib-dir)
+  "Find jerboa lib directory from current process library-directories."
+  (let loop ((dirs (library-directories)))
+    (if (null? dirs)
+      ;; Fallback: try common locations
+      (let ((home (or (getenv "HOME") "")))
+        (let try ((candidates (list (string-append home "/mine/jerboa/lib")
+                                    (string-append home "/.local/lib/jerboa")
+                                    "/usr/local/lib/jerboa")))
+          (if (null? candidates)
+            #f
+            (if (file-exists? (string-append (car candidates) "/std"))
+              (car candidates)
+              (try (cdr candidates))))))
+      (let ((d (car (car dirs))))
+        (if (and (string? d)
+                 (> (string-length d) 4)
+                 (file-exists? (string-append d "/std/repl.sls")))
+          d
+          (loop (cdr dirs)))))))
+
 (def (repl-start!)
-  "Spawn a Chez Scheme subprocess and return a repl-state."
-  ;; Chez open-process-ports returns: (write-stdin read-stdout read-stderr pid)
-  (let-values (((p-stdin p-stdout p-stderr pid)
-                (open-process-ports "scheme -q" (buffer-mode none) (native-transcoder))))
-    (close-port p-stderr)
-    ;; Store as (read-port . write-port) per struct contract
-    (make-repl-state (cons p-stdout p-stdin) 0 '())))
+  "Spawn a Jerboa REPL subprocess and return a repl-state."
+  ;; Build command with --libdirs for the jerboa stdlib
+  (let* ((libdir (jerboa-lib-dir))
+         (libdirs-arg
+           (if libdir
+             (string-append " --libdirs " libdir)
+             ""))
+         (cmd (string-append "scheme -q" libdirs-arg)))
+    ;; Chez open-process-ports returns: (write-stdin read-stdout read-stderr pid)
+    (let-values (((p-stdin p-stdout p-stderr pid)
+                  (open-process-ports cmd (buffer-mode none) (native-transcoder))))
+      (close-port p-stderr)
+      (let ((rs (make-repl-state (cons p-stdout p-stdin) 0 '())))
+        ;; Boot the jerboa REPL: import and start it
+        (repl-send! rs "(import (std repl))")
+        (repl-send! rs "(jerboa-repl)")
+        ;; Disable ANSI colors — QScintilla/TUI can't render escape codes
+        (repl-send! rs ",set color off")
+        rs))))
+
+;;;============================================================================
+;;; ANSI escape code stripping
+;;;============================================================================
+
+(def esc-char (integer->char 27))  ;; ESC = 0x1B
+(def bel-char (integer->char 7))   ;; BEL = 0x07
+
+(def (strip-ansi str)
+  "Remove ANSI escape sequences from a string.
+   Handles CSI sequences (ESC [ ... final-byte) and OSC sequences."
+  (let ((len (string-length str))
+        (out (open-output-string)))
+    (let loop ((i 0))
+      (when (< i len)
+        (let ((ch (string-ref str i)))
+          (if (char=? ch esc-char)
+            ;; Skip ESC sequence
+            (if (< (+ i 1) len)
+              (let ((next (string-ref str (+ i 1))))
+                (cond
+                  ;; CSI: ESC [ ... (ends at letter 0x40-0x7E)
+                  ((char=? next #\[)
+                   (let skip ((j (+ i 2)))
+                     (if (>= j len)
+                       (loop j)
+                       (let ((c (string-ref str j)))
+                         (if (and (char>=? c #\@) (char<=? c #\~))
+                           (loop (+ j 1))
+                           (skip (+ j 1)))))))
+                  ;; OSC: ESC ] ... (ends at BEL or ST)
+                  ((char=? next #\])
+                   (let skip ((j (+ i 2)))
+                     (if (>= j len)
+                       (loop j)
+                       (let ((c (string-ref str j)))
+                         (if (or (char=? c bel-char)
+                                 (char=? c esc-char))
+                           (loop (+ j 1))
+                           (skip (+ j 1)))))))
+                  ;; Other ESC sequences (2 char)
+                  (else (loop (+ i 2)))))
+              (loop (+ i 1)))
+            (begin
+              (write-char ch out)
+              (loop (+ i 1)))))))
+    (get-output-string out)))
+
+;;;============================================================================
+;;; I/O
+;;;============================================================================
 
 (def (repl-send! rs input)
-  "Send a line of input to the Chez Scheme subprocess."
+  "Send a line of input to the Jerboa REPL subprocess."
   (let ((out-port (cdr (repl-state-process rs))))
     (put-string out-port input)
     (put-char out-port #\newline)
@@ -51,8 +136,8 @@
     (cons input (repl-state-history rs))))
 
 (def (repl-read-available rs)
-  "Read all available output from the Chez subprocess (non-blocking).
-   Returns a string, or #f if nothing available."
+  "Read all available output from the Jerboa REPL subprocess (non-blocking).
+   Strips ANSI escape codes. Returns a string, or #f if nothing available."
   (let ((in-port (car (repl-state-process rs))))
     (if (char-ready? in-port)
       (let ((out (open-output-string)))
@@ -62,13 +147,18 @@
               (unless (eof-object? ch)
                 (write-char ch out)
                 (loop)))))
-        (let ((s (get-output-string out)))
+        (let ((s (strip-ansi (get-output-string out))))
           (if (string-empty? s) #f s)))
       #f)))
 
 (def (repl-stop! rs)
-  "Shut down the Chez Scheme subprocess."
+  "Shut down the Jerboa REPL subprocess."
   (let ((in-port (car (repl-state-process rs)))
         (out-port (cdr (repl-state-process rs))))
+    ;; Try graceful shutdown first
+    (with-catch (lambda (_e) (void))
+      (lambda ()
+        (put-string out-port ",quit\n")
+        (flush-output-port out-port)))
     (with-catch (lambda (_e) (void)) (lambda () (close-port out-port)))
     (with-catch (lambda (_e) (void)) (lambda () (close-port in-port)))))