cli: stop drain-stdin!/escape-watcher from spinning on EOF

ober

67a3cbf06869d71b9e2ba3ef6bbfc33f8b524581

diff --git a/src/jcode/ui/cli.ss b/src/jcode/ui/cli.ss
index 293577b..20c62f1 100644
--- a/src/jcode/ui/cli.ss
+++ b/src/jcode/ui/cli.ss
@@ -314,11 +314,14 @@ EXAMPLES:
   (system "stty cbreak -echo 2>/dev/null"))
 
 (def (drain-stdin!)
+  ;; char-ready? returns #t at EOF and read-char returns the eof-object,
+  ;; so a naive loop spins at 100% CPU when stdin is closed (piped input).
   (guard (e [#t (void)])
     (let loop ()
       (when (char-ready? (current-input-port))
-        (read-char (current-input-port))
-        (loop)))))
+        (let ((c (read-char (current-input-port))))
+          (unless (eof-object? c)
+            (loop)))))))
 
 (def (start-escape-watcher!)
   "Spawn thread that polls stdin in cbreak mode for ESC key."
@@ -330,9 +333,14 @@ EXAMPLES:
           (unless (car *stream-abort*)
             (if (char-ready? (current-input-port))
               (let ((c (read-char (current-input-port))))
-                (when (and (char? c) (= (char->integer c) 27))
-                  (set-car! *stream-abort* #t))
-                (unless (car *stream-abort*) (loop)))
+                ;; EOF on stdin means no more keystrokes will ever arrive
+                ;; (e.g. piped input). Exit instead of spinning.
+                (cond
+                  ((eof-object? c) (void))
+                  ((and (char? c) (= (char->integer c) 27))
+                   (set-car! *stream-abort* #t))
+                  (else
+                   (unless (car *stream-abort*) (loop)))))
               (begin
                 ;; thread-sleep!; (sleep (make-time ...)) is broken
                 ;; because the prelude shadows make-time.