The osc8-link implementation and its test both had doubled
backslashes (\\ -> \\) in their format strings, producing two
literal backslashes at runtime instead of the single backslash
needed for the OSC 8 string terminator (ST = ESC \).
Fix: change \\ to \\ in both the implementation and test format
strings. The tangled .lisp files now have correct escaped
backslashes (\) producing one backslash in the runtime string.
Additionally clean up a patch artifact that left a stray backslash
before the opening quote.
Add :mode parameter to defslot with three behaviors:
- :stack (default) — accumulate all registrations, render in order
- :replace — each registration replaces previous entries
- :single-winner — first registration wins, rest ignored
Mode is set on first defslot call and frozen for subsequent calls
to prevent conflicting mode specifications from different plugins.
Store slot data as plist (:mode <keyword> :entries <list>) instead
of bare entries list.
Add 5 new tests covering mode-specific behavior. All 9 slot tests
pass. All 13 suites pass at 100%.
- Create org/integration-tests.org (15 blocks, per-test prose)
- Add Markdown tests section to org/markdown-renderer.org (11 test blocks)
- Delete deprecated src/components/input-tests.lisp stub
- Update README.org: tree diagram, literate programming section,
development commands, remove stale test counts
All 13 test suites pass at 100%. Zero .lisp files without org origin.
The combined org file had no unique content — all prose and code were
already in scrollbox.org, tabbar.org, and container-package.org. The
old file's code blocks had the pre-bugfix render/draw-scrollbars
versions and all had :tangle no.
Also update README.org and ARCHITECTURE.org references from
scrollbox-tabbar.org to the individual org files.
Distribute the literate prose from the old combined scrollbox-tabbar.org
into three individual module org files:
- scrollbox.org: ScrollBox class, render, scrollbars, bug fixes,
plus the combined test suite (tangles scrollbox-tabbar-tests.lisp)
- tabbar.org: TabBar class, navigation, keyboard handler, render
- container-package.org: Package definition and exports
The old scrollbox-tabbar.org is retained as a documentation archive
with all code blocks set to :tangle no and a redirecting note.
Fixes the draw-scrollbars code block to use the post-bugfix version
(with layout-node origin offset ox/oy), matching the working code.
All 13 test suites pass at 100%.
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.
org/dirty.org is now the source of truth for dirty.lisp and
dirty-tests.lisp. The process:
Overview → Contract → Tests → Implement → Tangle → Test (GREEN)
Hand-written .lisp files were deleted and regenerated from org alone
to prove the pipeline works.
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.