The text-input widget now renders multi-line word-wrapped text using
cl-tty.box:word-wrap instead of single-line truncation. The cursor
position is computed from the wrapped lines using the same algorithm
as position-cursor but now lives in the library where it belongs.
This is the critical step that enables passepartout to replace its
ad-hoc view-input + position-cursor with a simple (render input be) call.
Placeholder text is shown when value is empty, drawn with :dim style.
Block cursor (█) at the correct word-wrapped position. All tests pass
at 100% including integration tests.
The mouse-event struct was already in cl-tty.input. All mouse handling
logic (mouse-mixin, hit-test, selection, clipboard, link detection)
was in a separate cl-tty.mouse package. Moved everything into the
input package where the struct lives, eliminating one package boundary.
Changes:
- absorb mouse-mixin, handle-mouse-event, hit-test, selection struct,
selection variables/functions, cell-link-at, open-link-at into
text-input.org (tangled to input.lisp)
- update cl-tty.input defpackage with mouse exports
- mouse tests merged into INPUT-SUITE (appended to input-tests.lisp)
- delete mouse.org, mouse-package.lisp, mouse.lisp, mouse-tests.lisp
- update ASDF, run-all-tests.lisp, scripts to drop mouse references
All test suites pass at 100% (INPUT-SUITE: 102 tests, +6 from mouse)
- Replace make-alien unsigned-char buffer with make-array + vector-sap
to avoid SBCL alien type mismatch between signed-char and unsigned-char
- Convert timeout seconds to fixnum milliseconds for unix-simple-poll
(was passing float 0.1, broke on fixnum-typed sb-unix:to-msec)
- Both fixes make read-raw-byte work on SBCL 2.5.2.debian
Application code (passepartout TUI) calls draw-text with a framebuffer
(2D array) as the first argument, but draw-text only had methods for
framebuffer-backend CLOS instances. Added a method on array that sets
cells directly on the framebuffer array, matching make-framebuffer's
return type.
- Add %read-digits to read multi-digit parameters from raw terminal bytes
- Add %parse-sgr-mouse to decode ESC[<Cb;Cx;CyM/m SGR mouse sequences
into mouse-event structs with :press/:release type and :left/:middle/
:right/:scroll-up/:scroll-down/:drag button classification
- Modified parse-csi-sequence to detect the < marker (0x3C) and
delegate to %parse-sgr-mouse instead of treating it as key input
- Coordinates converted from 1-based (terminal protocol) to 0-based
(framebuffer convention)
- All 12 test suites pass at 100% (461 checks, no regressions)
- Org source (text-input.org) updated as the source of truth
The tangled handle-text-input used (key-event-text event) for character
insertion, but the test suite creates key events with :code not :text.
Restored the original handle-text-input which uses
(code-char (key-event-code event)) — matching the test expectations.
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.
org/text-input.org: remove (declare (ignore w)) from textarea render;
add truncation to text-input render (subseq display 0 w)
org/mouse.org: hit-test now uses component-layout-node and recurses
into children for deepest-match hit testing
org/select.org: render reads layout-node-x/y instead of hardcoded (0,0)
org/scrollbox-tabbar.org: tabbar render reads layout-node-x/y
instead of hardcoded (0,0); x-pos starts at x offset
All 4 org files tangled clean. 392 tests pass.
CRITICAL: case b → cond in %read-event (input.lisp:280)
case with (and ...) predicate clauses treats keys as eql-compared
atoms — all range clauses were dead code. Every Ctrl+letter and
printable ASCII fell through to :unknown. text-input/textarea
widgets were non-functional with real terminal input. No test
coverage of %read-event masked this.
HIGH: Theme resolution wired (backend/modern.lisp, theme.lisp)
sgr-fg/sgr-bg now fall back to *theme-colors* hash for semantic
keywords (:accent, :text-muted, :background-element). *theme-colors*
exported from cl-tty.backend. load-preset populates it from preset
hex values. Previously all themed render output was invisible.
HIGH: SGR mouse parser wired (input.lisp:210-215)
parse-sgr-mouse was defined but never called. Now %read-escape-sequence
detects ESC[< prefix and routes to parse-sgr-mouse. Mouse drags,
releases, and scroll events now parse correctly.
MEDIUM: Rendering stubs replaced
- scrollbox: delegates to (render child backend) with position
offset via unwind-protect (was debug string 'child at ~D')
- text-input: draws value/placeholder at layout position
- textarea: draws visible lines at layout position
MEDIUM: hit-test uses component-layout-node (mouse.lisp:18-31)
Was checking nonexistent x/y/width/height slots. Now reads
layout-node-x/y/w/h via component-layout-node generic.
MEDIUM: test runner exit code (run-all-tests.lisp, cl-tty.asd)
run-all-tests.lisp exits 1 if any suite fails.
asdf:test-system exits 1 on failure.
Renamed :cl-tty-tests to :cl-tty/test (ASDF convention).
MEDIUM: draw-border respects x/y on simple-backend (simple.lisp:42-53)
Was writing to cursor position only. Now uses newlines+spaces
to reach specified coordinates (no escape sequences needed).
LOW: TabBar truncation off-by-one fixed (tabbar.lisp:47)
>= changed to > to avoid cutting tabs 2 chars early.
LOW: Scrollbar coordinates absolute (scrollbox.lisp:61-73)
Scrollbar drawn at viewport-relative (0,0). Now adds layout
node x/y offset for correct terminal positioning.
LOW: backend-write calls finish-output (modern.lisp:169)
LOW: load-preset no longer flips theme-mode (theme.lisp:43-45)
Mode toggle caused load-preset to load wrong variant on
second call.
All backported to org source files (org/text-input.org,
org/scrollbox-tabbar.org) so tangling produces matching .lisp.
392 tests pass, exit code 0.
37 per-function code blocks with prose explaining design reasoning,
edge cases, and CL traps. Combined tangle blocks at end for actual
compilation.
New scripts/tangle.py: reliable Python tangler (emacs --batch failed).
Added: %split-string, %join-lines, tangle helper.
CL traps documented in org prose:
- defstruct generates keyword constructors (no :constructor needed)
- case with strings uses EQL — use cond + string=
- CL strings: no \n escape — use (string #\Newline)
- FiveAM closure capture — use list boxing
- read-byte is package-locked — use read-raw-byte
- ASDF compile-file stricter than LOAD — debug with LOAD
60 tests, 100% GREEN.