v0.15.1: EOF/Escape fixes, box title rendering, full feature verification

Bug fixes:
  - read-raw-byte now returns (values nil :eof) on stdin EOF
    instead of just nil, so callers can distinguish EOF from
    timeout.  Previously, non-TTY stdin (pipes, /dev/null)
    caused a busy-spin: sb-posix:read returned 0 immediately,
    read-raw-byte returned nil, the demo loop treated nil as
    'no event yet' and spun at 100% CPU producing 86MB of
    repeated rendering frames.

  - %read-escape-sequence now uses a 50ms timeout on the first
    follow-up byte to resolve the classic Escape-key ambiguity:
    a lone Escape press returned an :escape key-event instead of
    blocking indefinitely on VMIN=1 VTIME=0.  All callers
    (SS3, CSI, Alt+char) propagate :eof instead of faking
    :escape events when EOF occurs mid-sequence.

  - parse-csi-params now uses multiple-value-bind on read-raw-byte
    to preserve the :eof signal through CSI parsing.

  - simple-backend draw-border now renders :title on the top
    edge instead of declaring it (ignore).  The title was
    silently swallowed — the box rendered with the right border
    frame but the title text was never written.

  - demo.lisp: removed 'q' as quit key (conflicted with text
    input).  Only Esc and Ctrl+C quit.  Widget event forwarding
    scoped to tab 1 (Widgets tab).  EOF handling in main loop.
  - Stale help text (still said 'q/esc: quit') updated.

Verification infrastructure:
  - PTY-based demo test (17 checks) spawns the demo in a real
    pseudo-terminal, sends actual keystrokes, reads terminal
    output back.  Verifies: startup rendering, tab switching,
    key dispatch, 'q' doesn't quit, Escape quits via timeout,
    Ctrl+C quits, EOF clean exit, no busy-spin.

  - API feature verification (29 checks) exercises every major
    component through the actual exported API: Simple backend,
    Box with title, Text attributes, draw-rect, TextInput
    (insert/backspace/cursor/Ctrl-A/E), TextArea, key/mouse
    events, Layout flex, Markdown, Theme presets (dark/light/
    nord), Select filtering, Dialog stack, Mouse hit-test,
    Framebuffer, Dirty tracking, Modern backend, draw-ellipsis/
    draw-link, Render dispatch, Detection, Capabilities.

  - Testing pattern saved as skill (tui-pty-testing) for reuse.

Unit tests: 392/392 passing.  All 12 test suites green.
This commit is contained in:
Hermes Agent
2026-05-12 10:58:27 +00:00
parent eede03ee3f
commit 7f4f712399
5 changed files with 117 additions and 80 deletions

View File

@@ -50,7 +50,7 @@
(draw-text backend (+ x 2) (+ y 10) " deps: zero FFI, zero ncurses, pure CL" :bright-cyan nil)
(draw-text backend (+ x 2) (+ y 12) "Controls" :bright-white nil :bold t)
(draw-text backend (+ x 2) (+ y 13) " Tab / arrows switch tabs" nil nil)
(draw-text backend (+ x 2) (+ y 14) " q / Ctrl+C / Esc quit" nil nil)
(draw-text backend (+ x 2) (+ y 14) " Ctrl+C / Esc quit" nil nil)
(draw-text backend (+ x 2) (+ y 15) " mouse click/drag select text (test SGR mouse)" nil nil))
(defun render-tab-widgets (backend x y w h input ta)
@@ -97,7 +97,7 @@
(ctrl (key-event-ctrl event)))
(log-append "Key: ~a (ctrl=~a alt=~a shift=~a)" key ctrl (key-event-alt event) (key-event-shift event))
(cond
((or (eql key :|Q|) (and ctrl (eql key :|C|)) (eql key :escape))
((or (and ctrl (eql key :|C|)) (eql key :escape))
(setf (getf *app* :running) nil) t)
((eql key :tab)
(incf (getf *app* :tab))
@@ -108,10 +108,11 @@
((eql key :right)
(incf (getf *app* :tab))
(when (> (getf *app* :tab) 2) (setf (getf *app* :tab) 0)) t)
;; Forward key to widgets for testing
(t (handle-text-input (getf *app* :input) event)
(handle-textarea-input (getf *app* :textarea) event)
t))))
;; Forward key to widgets only when on the Widgets tab
(t (when (= (getf *app* :tab) 1)
(handle-text-input (getf *app* :input) event)
(handle-textarea-input (getf *app* :textarea) event))
t))))
(mouse-event
(log-append "Mouse: ~a btn=~a pos=(~d,~d)" (mouse-event-type event)
(mouse-event-button event) (mouse-event-x event) (mouse-event-y event))
@@ -133,7 +134,7 @@
(backend-clear backend)
;; Title bar
(draw-border backend 2 1 (- w 4) 3 :style :double :title " cl-tty v0.15.0 ")
(draw-text backend 4 2 "arrows/tab: tabs type: test input mouse: test SGR q/esc: quit"
(draw-text backend 4 2 "arrows/tab: tabs type: test input mouse: test SGR Esc/Ctrl+C: quit"
:bright-white nil)
;; Tab bar
(loop for (label . idx) in '((" Home " . 0) (" Widgets " . 1) (" Console " . 2))
@@ -164,8 +165,9 @@
(finish-output *standard-output*)
;; Read event — blocks until a key or mouse event arrives
(let ((event (read-event backend)))
(when event
(handle-event event))))
(cond
((eq event :eof) (setf (getf *app* :running) nil))
(event (handle-event event)))))
(shutdown-backend backend))))
(run-demo)