From 183aeeedb810b3d72e7daa44ebb42ca76228e43a Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Wed, 6 May 2026 10:11:52 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20backspace=20+=20TUI=20rendering=20?= =?UTF-8?q?=E2=80=94=20normalize=20ncurses=20codes,=20initial=20redraw,=20?= =?UTF-8?q?socket=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- lisp/gateway-tui-main.lisp | 77 +++++++++++++++++++++++--------------- org/gateway-tui-main.org | 77 +++++++++++++++++++++++--------------- 2 files changed, 94 insertions(+), 60 deletions(-) diff --git a/lisp/gateway-tui-main.lisp b/lisp/gateway-tui-main.lisp index c239c97..117ff03 100644 --- a/lisp/gateway-tui-main.lisp +++ b/lisp/gateway-tui-main.lisp @@ -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"))) diff --git a/org/gateway-tui-main.org b/org/gateway-tui-main.org index 4677430..51a3c5a 100644 --- a/org/gateway-tui-main.org +++ b/org/gateway-tui-main.org @@ -29,11 +29,14 @@ Event handlers + daemon I/O + main loop. (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 @@ -227,7 +230,7 @@ Event handlers + daemon I/O + main loop. #+begin_src lisp (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 *") @@ -257,20 +260,24 @@ Event handlers + daemon I/O + main loop. (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)))) @@ -348,8 +355,8 @@ Event handlers + daemon I/O + main loop. (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 @@ -365,19 +372,29 @@ Event handlers + daemon I/O + main loop. ;; 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))))) @@ -386,7 +403,7 @@ Event handlers + daemon I/O + main loop. (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))))) @@ -395,7 +412,7 @@ Event handlers + daemon I/O + main loop. (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))))) @@ -404,7 +421,7 @@ Event handlers + daemon I/O + main loop. (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 @@ -412,7 +429,7 @@ Event handlers + daemon I/O + main loop. (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 @@ -421,7 +438,7 @@ Event handlers + daemon I/O + main loop. (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)))) @@ -430,7 +447,7 @@ Event handlers + daemon I/O + main loop. (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)))) @@ -442,7 +459,7 @@ Event handlers + daemon I/O + main loop. ;; 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")))