fix: threaded keyboard reader, key events via queue

- Keyboard input now runs in a separate bordeaux-thread that
  reads from fd 0 via blocking read-char and queues :key events
- Main loop processes :key events from drain-queue alongside
  :daemon and :disconnected events
- Removed blocking read-char/with-timeout from main loop (caused
  freeze when with-timeout couldn't interrupt the read syscall)
- Added full dialog key routing in the key event handler
- Added debug logging for key events (tui-keys.log)
This commit is contained in:
2026-05-14 10:32:46 -04:00
parent a8f8d841a4
commit 0ad9d3bdb5

View File

@@ -864,6 +864,30 @@
(cl-tty.input:with-raw-terminal (cl-tty.input:with-raw-terminal
(cl-tty.backend:with-terminal (be w h) (cl-tty.backend:with-terminal (be w h)
(let ((tty (sb-sys:make-fd-stream 0 :input t :buffering :none))) (let ((tty (sb-sys:make-fd-stream 0 :input t :buffering :none)))
;; Keyboard reader thread: reads from fd 0 via blocking read-char
;; and queues :key events for the main loop to process.
(bt:make-thread
(lambda ()
(loop
(let* ((raw-ch (read-char tty nil nil)))
(unless raw-ch (return)) ; EOF → stream closed, exit
(let ((code (char-code raw-ch)))
(queue-event
(list :type :key
:payload (list :code code
:ch (cond
((= code 13) :enter)
((= code 10) :enter)
((= code 27) :escape)
((= code 9) :tab)
((or (= code 127) (= code 8)) :backspace)
((and (>= code 1) (<= code 26))
(intern (string-upcase
(format nil "CTRL-~a"
(code-char (+ #x60 code))))
:keyword))
(t raw-ch)))))))))
:name "tui-keyboard")
;; Log backend info and terminal dimensions ;; Log backend info and terminal dimensions
(let ((backend-type (if (typep be 'cl-tty.backend:modern-backend) (let ((backend-type (if (typep be 'cl-tty.backend:modern-backend)
"modern" "simple"))) "modern" "simple")))
@@ -886,28 +910,14 @@
((eq (getf ev :type) :disconnected) ((eq (getf ev :type) :disconnected)
(setf (st :connected) nil (setf (st :connected) nil
(st :busy) nil) (st :busy) nil)
(add-msg :system "* Connection lost — type /reconnect to retry *")))) (add-msg :system "* Connection lost — type /reconnect to retry *"))
;; Read key input via blocking read-char with 0.1s timeout ((eq (getf ev :type) :key)
;; (sb-unix:unix-simple-poll returns NIL on fd 0 in this SBCL, (let* ((payload (getf ev :payload))
;; so read-char-no-hang and read-event never fire. Raw blocking (ch (getf payload :ch)))
;; read-char with sb-ext:with-timeout is the reliable fallback.) (with-open-file (d "/tmp/tui-keys.log"
(handler-case :direction :output :if-exists :append
(sb-ext:with-timeout 0.1 :if-does-not-exist :create)
(let* ((raw-ch (read-char tty nil 'eof)) (format d "KEY EVENT ch=~s type=~s~%" ch (type-of ch)))
(code (when (characterp raw-ch) (char-code raw-ch))))
(when code
(let ((ch (cond
((= code 13) :enter)
((= code 10) :enter)
((= code 27) :escape)
((= code 9) :tab)
((or (= code 127) (= code 8)) :backspace)
((and (>= code 1) (<= code 26))
(intern (string-upcase
(format nil "CTRL-~a"
(code-char (+ #x60 code))))
:keyword))
(t raw-ch))))
(case ch (case ch
(:CTRL-Q (setf (st :running) nil)) (:CTRL-Q (setf (st :running) nil))
(:CTRL-P (command-palette-show-commands)) (:CTRL-P (command-palette-show-commands))
@@ -946,7 +956,7 @@
(setf (cl-tty.select:select-filter sel) (setf (cl-tty.select:select-filter sel)
(subseq f 0 (1- f)))))))) (subseq f 0 (1- f))))))))
(on-key ch)))))))) (on-key ch))))))))
(sb-ext:timeout ())) ;; Keyboard input now comes via events; no blocking read needed
;; Re-query terminal size once after daemon handshake ;; Re-query terminal size once after daemon handshake
(when (and (st :connected) (st :daemon-version) (not (st :size-queried))) (when (and (st :connected) (st :daemon-version) (not (st :size-queried)))
(multiple-value-setq (w h) (cl-tty.backend:backend-size be)) (multiple-value-setq (w h) (cl-tty.backend:backend-size be))