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

@@ -57,9 +57,10 @@ SBCL's ~sb-posix~ provides the POSIX terminal APIs (~tcgetattr~,
~with-raw-terminal &body body~ — macro. Save → set raw → body → restore
(via ~unwind-protect~).
~read-raw-byte &key timeout~ → byte or NIL.
~read-raw-byte &key timeout~ (values byte-or-nil reason).
Read one byte from fd 0. Blocks indefinitely when timeout=NIL.
Returns NIL on timeout. Uses ~sb-posix:read~.
Returns (values byte NIL) on success, (values NIL :TIMEOUT) on timeout,
(values NIL :EOF) when stdin is closed or /dev/null.
~parse-csi-params~ → (values params final-byte raw-string).
Read bytes from stdin until a final CSI byte (0x40-0x7E).
@@ -70,14 +71,17 @@ SBCL's ~sb-posix~ provides the POSIX terminal APIs (~tcgetattr~,
Converts button codes (0=left, 1=middle, 2=right, 32=motion)
and tracks press vs release vs drag.
~%read-escape-sequence~ → key-event.
Called after reading ESC (0x1b). Dispatches:
~%read-escape-sequence~ → key-event or :eof.
Called after reading ESC (0x1b). Uses a 50ms timeout on the first
follow-up byte to resolve Escape ambiguity (lone Escape vs start of
CSI/SS3 sequence). Dispatches:
- timeout → :escape key event
- ESC O X → SS3 (F1-F4)
- ESC [ ... → CSI (cursors, function keys, mouse)
- ESC ESC → Alt+Escape
- ESC printable → Alt+letter
~%read-event &key timeout~ → key-event, mouse-event, or NIL.
~%read-event &key timeout~ → key-event, mouse-event, :eof, or NIL.
Top-level reader. Handles:
- Printable ASCII (0x20-0x7e) → key :A, :B, ..., :~
- Ctrl letters (0x01-0x1a) → :A with ctrl=T