The %query-terminal-size function sent \033[18t and tried to read
the response via read-char-no-hang on fd 0, which always returns nil
in this SBCL environment. The response leaked into user input,
displaying garbled CSI sequences. Rely on ioctl only.
- Disabled \033[?u kitty keyboard protocol in modern-backend
(converts all keys to escape sequences, breaking Ctrl+letter dispatch)
- Fixed parse-csi-sequence: use multiple-value-bind instead of let*
with destructuring-bind (lost secondary return value from read-param)
- Fixed parse-csi-params format string: pass char-code of terminator
as distinct argument for ~d, keeping the character for ~C
- Added %query-terminal-size in classes.lisp: ANSI CSI 18t fallback
for terminal size detection when ioctl fails or returns zero
The original code called unix-simple-poll then unconditionally
called unix-read, ignoring the poll result. When poll returned
nil (no data), unix-read would block indefinitely. Fixed by
checking poll result: only read if poll says data is ready.
Same pattern as the draw-text array fix. Application code may call
backend-clear with a framebuffer array instead of a backend instance.
The array method clears all cells to default blank state.
- 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.
with-terminal macro was only in tangled .lisp (not .org). suspend-backend
and resume-backend generics + simple-backend methods + tests were also
in hand-edited .lisp only. All three added to org/backend-protocol.org
with proper prose, following the literate programming discipline.
Also added suspend/resume assertions to simple-backend-lifecycle test suite.
- 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
Bug fixes:
- Fix OSC8 format strings (backslash escape layering) in modern-backend.org
- Test format string had single backslash instead of double, causing
unclosed CL string that cascaded through 3 subsequent test forms
- Implementation format string had leading escaped quote (not a string
opener) and triple-backslash ending (also not a string terminator)
- Fix missing closing parens in border-char-rounded and border-char-double tests
- Fix ASDF input-tests pathname (file lives in tests/, not src/components/)
New features:
- Implement suspend-backend / resume-backend protocol methods
- modern-backend: exit/enter alt screen, re-enable mouse/kitty/bracketed-paste
- simple-backend: no-ops (no terminal state to preserve)
Infrastructure:
- Update test suite to cover suspend/resume (backend + modern-backend suites)
- 454 checks, 100% pass across 14 test suites
Add assert to reject invalid mode keywords on first registration
instead of silently storing them and only crashing later in
slot-render's ecase. Valid modes: :stack, :replace, :single-winner.
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.
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.
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.
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.
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.