Preserve interactive typeahead during commands

ober

e5f8689c582c304b3dd3623e418d6b8d3b27a173

diff --git a/main.ss b/main.ss
index 70bdb36..e6c9f21 100644
--- a/main.ss
+++ b/main.ss
@@ -26,8 +26,7 @@
         :jsh/startup
         :jsh/arithmetic
         :jsh/ffi
-        (only (std os platform) platform-cpu-count)
-        (only (compat gambit) tty-mode-set!))
+        (only (std os platform) platform-cpu-count))
 
 ;;; --- Session state parameters ---
 
@@ -391,12 +390,10 @@
                             (let ([action (trap-get "ERR")])
                               (when (string? action)
                                 (execute-input action env))))))))))))
-           ;; Restore terminal to sane state after command
-           ;; (programs like top/vim may leave echo off or raw mode on).
-           ;; Must use Gambit's tty-mode-set! to keep port state in sync;
-           ;; raw tcsetattr behind Gambit's back desynchronizes its cache.
+           ;; Restore terminal to sane state after command without flushing
+           ;; queued typeahead from the terminal input buffer.
            (when (= (ffi-isatty 0) 1)
-             (tty-mode-set! (current-input-port) #t #t #f #f 0))
+             (ffi-termios-restore 0 0))
            (loop (+ cmd-num 1))))))))
 
 (def (output-to-string proc)
diff --git a/test/test-binary.sh b/test/test-binary.sh
index ba23124..833016f 100755
--- a/test/test-binary.sh
+++ b/test/test-binary.sh
@@ -140,6 +140,65 @@ PY
         fail=$((fail + 1))
         printf 'FAIL: interactive REPL multi-line continuation (for loop)\n'
     fi
+
+    if python3 - "$BINARY" <<'PY'
+import os, pty, re, select, sys, time
+
+binary = sys.argv[1]
+master, slave = pty.openpty()
+pid = os.fork()
+if pid == 0:
+    os.close(master)
+    os.setsid()
+    os.dup2(slave, 0)
+    os.dup2(slave, 1)
+    os.dup2(slave, 2)
+    os.close(slave)
+    env = os.environ.copy()
+    env['PS1'] = 'jsh-test> '
+    env['PS2'] = 'jsh-more> '
+    os.execve(binary, [binary, '-i'], env)
+    sys.exit(1)
+os.close(slave)
+
+out = b''
+
+def read_for(timeout):
+    global out
+    end = time.time() + timeout
+    while time.time() < end:
+        r, _, _ = select.select([master], [], [], 0.05)
+        if not r:
+            continue
+        try:
+            data = os.read(master, 4096)
+        except OSError:
+            break
+        if not data:
+            break
+        out += data
+
+read_for(1.0)
+os.write(master, b'sleep 1\n')
+time.sleep(0.15)
+os.write(master, b'x=TYPEAHEAD_OK; printf "ran:%s\\n" "$x"\n')
+read_for(2.5)
+os.write(master, b'exit\n')
+read_for(0.5)
+_, status = os.waitpid(pid, 0)
+
+text = re.sub(r'\x1b\[[0-9;?]*[a-zA-Z]', '', out.decode('utf-8', errors='replace'))
+if 'ran:TYPEAHEAD_OK' in text and os.WIFEXITED(status):
+    sys.exit(0)
+print(text, file=sys.stderr)
+sys.exit(1)
+PY
+    then
+        pass=$((pass + 1))
+    else
+        fail=$((fail + 1))
+        printf 'FAIL: interactive REPL preserves typeahead while foreground command runs\n'
+    fi
 else
     echo "SKIP: interactive REPL tests require python3"
 fi