Previous version had 14 failing checks due to wrong function names:
- Theme: load-preset with :keyword mode, not nonexistent load-default-*-preset
- Select: setf select-filter + select-filtered-options with 1 arg
- Dialog: push-dialog/pop-dialog + dialog-title on car of *dialog-stack*
- Mouse: make-box has no :x/:y initargs, use default constructor
- Framebuffer: draw-text on framebuffer-backend, not draw-text-on-fb
- Dirty: dirty-p, not component-dirty-p
- Theme functions in cl-tty.box package, not cl-tty.rendering
Also add ci-watchdog.sh for 15-min polling CI.
All 29 checks now pass.
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.
Raw terminal mode must be set by the parent process (the shell),
not from inside SBCL. sb-ext:run-program subprocesses cannot
reliably access the controlling terminal for stty operations.
./demo.sh sets raw mode via stty, runs sbcl --script demo.lisp,
and restores terminal state on exit (EXIT, INT, TERM).
demo.lisp no longer calls with-raw-terminal — it assumes the
calling shell has already set raw mode.
Added ./demo shell script that sets raw mode via stty before running
the Lisp demo and restores it on exit (including SIGINT/SIGTERM).
demo.lisp no longer attempts to set raw mode from inside SBCL —
terminal raw mode is the shell's responsibility. This avoids the
recurring problem of sb-ext:run-program + stty not being able to
access the controlling terminal from inside sbcl --script.
The -F flag isn't available on all stty implementations. Using
shell stdin redirect (stty ... < /dev/tty) via /bin/sh is more
portable and doesn't depend on run-program preserving the
controlling terminal across subprocess boundaries.
stty now operates on /dev/tty explicitly (-F flag) instead of
relying on stdin inheritance. This is more reliable in SBCL's
--script mode where stdin may be handled differently by run-program.
Also ensures stty always targets the controlling terminal regardless
of how the subprocess is spawned.
set-raw-mode now uses (stty raw -echo ...) via sb-ext:run-program
instead of sb-posix:tcgetattr/tcsetattr + termios flag manipulation.
The sb-posix termios API changed between SBCL versions (termios-cc
accessor went from 2-arg to 1-arg), and tcgetattr fails in some
container/PTY environments.
Stty is available on every Unix and is independent of SBCL's
sb-posix version. set-raw-mode errors if stty -g returns empty
(no real terminal attached). restore-terminal-state is a no-op
when called with nil.
make-raw-termios (input.lisp:66-67): termios-cc accessor in SBCL 2.5.x
takes one arg (the struct) and returns the cc array. Use (aref ...)
to set individual control characters. Old code used 3-arg setf form
that no longer works and produced style warnings.
demo.lisp: Now exits with a clear error message when raw mode can't
be established, rather than running in broken pipe-safe mode where
escape sequences are echoed and input is line-buffered.
No Quicklisp needed at all. Works from a fresh git clone with
just SBCL installed. Registering the current directory in ASDF's
central-registry is enough to find cl-tty.asd.
The demo now guards the quickload with a (find-package :cl-tty.backend)
check first, tries ql:quickload inside ignore-errors, and falls through
to direct (load cl-tty.asd) + (asdf:load-system :cl-tty) if the
package still isn't loaded. Works in --disable-debugger mode where
Quicklisp's SYSTEM-NOT-FOUND continuable error kills the process.
demo.lisp now registers the current directory as a quicklisp project
source and falls back to direct asdf:load-system if quicklisp can't
find cl-tty. Lets the demo run from a fresh git clone without
symlinking into ~/quicklisp/local-projects/.
demo.lisp:
- Removed ignore-errors wrapper: run-demo now returns normally,
followed by (uiop:quit 0) at top level — fixes exit code always 1 bug
- Manual set-raw-mode/unwind-protect/restore-terminal-state instead of
with-raw-terminal macro (safer in edge cases)
- Graceful fallback when raw mode fails: continues in pipe-safe mode
so the demo renders frames even without terminal control
- Simplified tab rendering, fixed textarea-lines display
The demo runs correctly in both interactive and pipe-safe modes.
In a real terminal: raw mode, keyboard/mouse event loop.
In pipe-safe mode: spins rendering frames (read-event returns nil).
Verified running: frames render correctly with borders, tabs, content,
status bar, and event counter.
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: Remove (declare (ignore w)) from textarea render (textarea.lisp:251)
w is used for horizontal truncation on the next line. Declaring it
ignored while using it is undefined behavior in CL (SBCL warns).
HIGH: hit-test recurses into children (mouse.lisp:18-34)
Was returning the root component for any click within its bounds,
ignoring nested widgets entirely. Now checks component-children
first, returning the deepest match.
MEDIUM: Select/TabBar position hardcoded to (0,0)
Both rendered at terminal origin regardless of layout position.
Now read layout-node-x/y for absolute positioning.
MEDIUM: Text-input truncation missing
Render drew full value string even when exceeding widget width.
Now truncates to (min (length display) w).
MEDIUM: X10 mouse release detection added (input.lisp:219-226)
X10 encoding uses button=3 for release. Was detecting all events
as press/drag. Now checks button=3 → :release.
MEDIUM: parse-csi-params handles private markers (input.lisp:128-131)
< = > ? characters (0x3c-0x3f) treated as parameter start markers
instead of accumulating bogus digit values. Latent trap removed.
Deferred (pre-existing design):
- Scrollbox visibility cy vs orig-y: match for column layout (common case)
- Nested scrollbox coordinates: assumes sequential layout positions
- text-input cursor drawing: feature, not bugfix
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.
New module: src/rendering/framebuffer.lisp (tangled from org/framebuffer.org)
- framebuffer-backend class: implements backend protocol by writing to
2D cell array instead of emitting escape sequences
- cell struct: per-cell state (char, fg, bg, bold, italic, underline, link-url)
- make-framebuffer / framebuffer-width / framebuffer-height
- draw-text, draw-rect, draw-border, draw-link, draw-ellipsis methods
- diff-framebuffers: compares two framebuffers, returns changed cells
- flush-framebuffer: diff + output changes to real backend
- with-scissor macro: clip drawing operations to rectangle
- cursor-move: added default no-op method for all backends
- 20 new tests, all passing (372 total)
Version bumped from 0.11.0 to 0.13.0.
License field set to GPL-3.0 in ASDF.
Root cause: normalize-box and slot :initforms used quoted literal
lists ('(...)) that were destructively modified by (setf (getf ...)).
Each call to normalize-box with a non-nil spec corrupted the shared
default list, causing all subsequent nodes with no explicit padding
to inherit the previous node's padding values.
Fix: replace all '(...) quoted literals with (list ...) constructor
calls — in normalize-box (3 paths) and in slot initforms for both
padding and margin.
All 11 test suites now pass: 358/358 checks, 0 failures.
- Three tabs: Home, Components, Stats with different content
- Real keyboard input: arrow keys to switch tabs, q to quit
- CSI escape sequence parsing for arrow keys
- Footer bar shows current tab position
- Tab bar highlights active tab in bright blue
- defslot: register render functions into named slots with ordering
- slot-render: call all registered render-fns for a slot
- Slot modes designed (stack/replace/single-winner) but mode dispatch
is implicit via the registration API
- slot-p, clear-slot, list-slots for lifecycle management
- Slots stored in a hash table keyed by string (equal test)
- 4 tests, 100% passing
- mouse-mixin class with on-mouse-down/up/move/scroll handler slots
- handle-mouse-event dispatches to the right handler by event type
- hit-test finds deepest component at (x,y) coordinates
- selection struct + get-selection + copy-to-clipboard
- SGR mouse parsing already existed in input system (mouse-event struct,
parse-sgr-mouse function, CSI dispatch in %read-escape-sequence)
- 3 tests, 100% passing
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.
Fixes from subagent review:
- ASDF version 0.3.0 → 0.4.0
- define-preset now checks (check-type name keyword) at macro-expand time
- load-preset-unknown-warns test now uses (signals warning ...) to
actually verify the warning fires (was false-positive before)
Fixes from subagent review:
- render-tests.lisp: added (in-suite box-suite) — tests were registered
to default suite, never executed by runner
- dirty-tests.lisp: same fix
- cl-tui.asd: version 0.2.0 → 0.3.0
- render.lisp: component-children default method (c t) nil for
protocol completeness (component-parent already had this)