fix: backspace + TUI rendering — normalize ncurses codes, initial redraw, socket fix
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s

- Backspace: get-char returns raw ncurses integers (263=KEY_BACKSPACE),
  not key structs. Use code-key + key-name to normalize codes >255
  to keywords, so (eq ch :backspace) actually matches.
- TUI blank screen: add initial redraw+refresh before the main loop.
  get-char blocks, so the first frame was never drawn on startup.
- connect-daemon: remove :element-type character (daemon listens in
  binary mode, mismatch caused hang). Add :timeout 10.
- Tests: use actual ncurses codes (343=KEY_ENTER, 263=KEY_BACKSPACE,
  9=TAB) instead of make-key or raw ascii codes.

TUI: 45/45 pass.
This commit is contained in:
2026-05-06 10:11:52 -04:00
parent 1f8b821287
commit 183aeeedb8
2 changed files with 94 additions and 60 deletions

View File

@@ -1,11 +1,14 @@
(in-package :passepartout.gateway-tui)
(defun on-key (&rest args)
;; Normalize: Croatoan returns key structs for special keys.
;; Extract the :name keyword so the rest of the handler can use eq.
;; Normalize: get-char returns raw ncurses integer codes (e.g. 263 for
;; backspace). Croatoan's code-key + key-name convert them to keywords
;; so the cond below can use eq.
(let* ((raw (car args))
(ch (if (typep raw 'croatoan:key)
(croatoan:key-name raw)
(ch (if (and (integerp raw) (> raw 255))
(let* ((k (code-key raw))
(name (and k (key-name k))))
(or name raw))
raw)))
(cond
;; Enter
@@ -193,7 +196,7 @@
(defun connect-daemon (&optional (host "127.0.0.1") (port 9105))
(handler-case
(let ((s (usocket:socket-connect host port :element-type 'character)))
(let ((s (usocket:socket-connect host port :timeout 10)))
(setf (st :stream) (usocket:socket-stream s) (st :connected) t)
(bt:make-thread (lambda () (reader-loop (st :stream))) :name "tui-reader")
(add-msg :system "* Connected *")
@@ -220,20 +223,24 @@
(swank-port (or (ignore-errors
(parse-integer (uiop:getenv "TUI_SWANK_PORT")))
4006)))
(setf (function-keys-enabled-p iw) t
(st :dirty) (list t t t))
(connect-daemon)
(when (> swank-port 0)
(handler-case
(progn
(ql:quickload :swank :silent t)
(funcall (find-symbol "CREATE-SERVER" "SWANK")
:port swank-port :dont-close t)
(add-msg :system
(format nil "* Swank ~d M-x slime-connect *" swank-port)))
(error ()
(add-msg :system "* Swank unavailable *"))))
(loop while (st :running) do
(setf (function-keys-enabled-p iw) t
(st :dirty) (list t t t))
(connect-daemon)
(when (> swank-port 0)
(handler-case
(progn
(ql:quickload :swank :silent t)
(funcall (find-symbol "CREATE-SERVER" "SWANK")
:port swank-port :dont-close t)
(add-msg :system
(format nil "* Swank ~d M-x slime-connect *" swank-port)))
(error ()
(add-msg :system "* Swank unavailable *"))))
;; Initial render before the main loop — otherwise the screen stays
;; blank until the first keystroke (get-char blocks).
(redraw sw cw ch iw)
(refresh scr)
(loop while (st :running) do
(dolist (ev (drain-queue))
(when (eq (getf ev :type) :daemon)
(on-daemon-msg (getf ev :payload))))
@@ -307,8 +314,8 @@
(dolist (ch '(#\t #\e #\s #\t))
(on-key (char-code ch)))
(fiveam:is (string= "test" (input-string)))
;; Simulate Enter key — Croatoan returns a key struct for :enter
(on-key (croatoan:make-key :name :enter))
;; Simulate Enter key — ncurses returns 343 (KEY_ENTER) when keypad is enabled
(on-key 343)
;; Input buffer should be cleared
(fiveam:is (string= "" (input-string)))
;; A user message should be in the message list
@@ -324,19 +331,29 @@
;; Type "/eval (+ 1 2)"
(dolist (ch (coerce "/eval (+ 1 2)" 'list))
(on-key (char-code ch)))
(on-key (croatoan:make-key :name :enter))
(on-key 343)
(let ((msgs (st :messages)))
(fiveam:is (>= (length msgs) 1))
(let ((last-msg (first msgs)))
(fiveam:is (eq :system (getf last-msg :role)))
(fiveam:is (search "=> 3" (getf last-msg :content))))))
(fiveam:test test-on-key-backspace
"Contract 1: on-key with Backspace removes last character from buffer."
(init-state)
(dolist (ch '(#\a #\b #\c))
(on-key (char-code ch)))
(fiveam:is (string= "abc" (input-string)))
;; ncurses returns 263 (KEY_BACKSPACE) when keypad is enabled
(on-key 263)
(fiveam:is (string= "ab" (input-string))))
(fiveam:test test-on-key-focus-command
"Contract 1: /focus command parses project name."
(init-state)
(dolist (ch (coerce "/focus myapp" 'list))
(on-key (char-code ch)))
(on-key (croatoan:make-key :name :enter))
(on-key 343)
(let ((msg (first (st :messages))))
(fiveam:is (eq :system (getf msg :role)))))
@@ -345,7 +362,7 @@
(init-state)
(dolist (ch (coerce "/scope memex" 'list))
(on-key (char-code ch)))
(on-key (croatoan:make-key :name :enter))
(on-key 343)
(let ((msg (first (st :messages))))
(fiveam:is (eq :system (getf msg :role)))))
@@ -354,7 +371,7 @@
(init-state)
(dolist (ch (coerce "/unfocus" 'list))
(on-key (char-code ch)))
(on-key (croatoan:make-key :name :enter))
(on-key 343)
(let ((msg (first (st :messages))))
(fiveam:is (eq :system (getf msg :role)))))
@@ -363,7 +380,7 @@
(init-state)
(dolist (ch (coerce "/ev" 'list))
(on-key (char-code ch)))
(on-key (croatoan:make-key :name :tab))
(on-key 9)
(fiveam:is (string= "/eval " (input-string))))
(fiveam:test test-on-key-tab-no-slash
@@ -371,7 +388,7 @@
(init-state)
(dolist (ch (coerce "hello" 'list))
(on-key (char-code ch)))
(on-key (croatoan:make-key :name :tab))
(on-key 9)
(fiveam:is (string= "hello" (input-string))))
(fiveam:test test-on-key-multiline
@@ -380,7 +397,7 @@
(dolist (ch (coerce "line1" 'list))
(on-key (char-code ch)))
(on-key (char-code #\\))
(on-key (croatoan:make-key :name :enter))
(on-key 343)
(fiveam:is (search "line1" (input-string)))
(fiveam:is (search (string #\Newline) (input-string))))
@@ -389,7 +406,7 @@
(init-state)
(dolist (ch (coerce "/help" 'list))
(on-key (char-code ch)))
(on-key (croatoan:make-key :name :enter))
(on-key 343)
(let ((msgs (st :messages)))
(fiveam:is (>= (length msgs) 3))
(fiveam:is (some (lambda (m) (search "/eval" (getf m :content))) msgs))))
@@ -401,7 +418,7 @@
;; Simulate sending a normal message (sets busy)
(dolist (ch (coerce "hello" 'list))
(on-key (char-code ch)))
(on-key (croatoan:make-key :name :enter))
(on-key 343)
(fiveam:is (eq t (st :busy)))
;; Simulate receiving an agent response (clears busy)
(on-daemon-msg '(:type :event :payload (:text "hi back")))