v0.15.0: Critical input/rendering fixes, subagent-reviewed #7
103
README.org
103
README.org
@@ -1,53 +1,74 @@
|
||||
#+TITLE: cl-tty — Reusable Common Lisp Terminal UI Framework
|
||||
#+STARTUP: content
|
||||
#+FILETAGS: :project:cl-tty:readme:
|
||||
# cl-tty — Terminal UI Framework for Common Lisp
|
||||
|
||||
* cl-tty
|
||||
Pure CL terminal UI framework. No ncurses, no FFI, no external dependencies.
|
||||
|
||||
A reusable Common Lisp framework for building rich terminal user interfaces.
|
||||
Built on croatoan (ncurses) with Yoga for Flexbox layout. Provides a component
|
||||
tree model with dirty-tracking, incremental rendering, layered keybinding,
|
||||
theme engine, and full mouse support — the primitives needed to match the TUI
|
||||
quality of Claude Code and OpenCode from Common Lisp.
|
||||
|
||||
** Why
|
||||
|
||||
Common Lisp has no reusable terminal UI framework at the level of Python's
|
||||
Rich/prompt_toolkit or Go's Bubble Tea. Every CL project that wants a
|
||||
terminal UI either builds ncurses from scratch or uses a text-only REPL.
|
||||
cl-tty fills that gap — a component library with Flexbox layout, semantic
|
||||
theming, layered keybinding, and full mouse support. Build a terminal UI once,
|
||||
reuse it everywhere.
|
||||
|
||||
Terminal UIs also work over SSH. A Qt or browser-based UI requires a local
|
||||
display. A cl-tty application runs remotely — same code, same components,
|
||||
accessible from anywhere.
|
||||
|
||||
** Architecture
|
||||
|
||||
```
|
||||
Application code (any CL project)
|
||||
└── cl-tty (layout, components, theme, events, dialogs)
|
||||
└── Yoga (Flexbox layout — C library via FFI)
|
||||
└── croatoan (ncurses terminal rendering)
|
||||
```lisp
|
||||
(ql:quickload :cl-tty)
|
||||
```
|
||||
|
||||
cl-tty depends only on croatoan and Yoga. It is not tied to any application.
|
||||
## Quick start
|
||||
|
||||
** Dependencies
|
||||
```lisp
|
||||
;; Create a modern terminal backend
|
||||
(let ((backend (make-instance 'cl-tty.backend:modern-backend)))
|
||||
(cl-tty.backend:initialize-backend backend)
|
||||
;; Backend is ready — write text, draw boxes, handle input
|
||||
(cl-tty.backend:shutdown-backend backend))
|
||||
```
|
||||
|
||||
- Common Lisp (SBCL tested)
|
||||
- croatoan — ncurses binding for terminal rendering
|
||||
- Yoga — Flexbox layout engine (C library, loaded via CFFI)
|
||||
- Quicklisp libraries as needed (ironclad for hashing, bordeaux-threads)
|
||||
## Architecture
|
||||
|
||||
** Status
|
||||
Two backends, one protocol:
|
||||
|
||||
v0.1.0 — Layout engine (in progress)
|
||||
- **modern-backend** — truecolor 24-bit, OSC 8 hyperlinks, DECICM sync,
|
||||
SGR mouse, kitty keyboard, bold/italic/underline, box-drawing chars
|
||||
- **simple-backend** — ASCII art, no color, universal compatibility
|
||||
|
||||
See ~docs/ROADMAP.org~ for the full release plan.
|
||||
Everything is pure escape sequences (no curses, no terminfo, no FFI).
|
||||
|
||||
** License
|
||||
## Components
|
||||
|
||||
| Component | What it does | Version |
|
||||
|-------------|------------------------------------------------------|---------|
|
||||
| Box | Bordered container with background, title | v0.2.0 |
|
||||
| Text | Styled text with word-wrap, spans | v0.2.0 |
|
||||
| ScrollBox | Scrollable viewport with scrollbars | v0.6.0 |
|
||||
| TabBar | Horizontal tab navigation | v0.6.0 |
|
||||
| Select | Dropdown with fuzzy filter, category headers | v0.7.0 |
|
||||
| TextInput | Single-line text input with readline keybindings | v0.5.0 |
|
||||
| TextArea | Multi-line input with undo/redo, selection | v0.5.0 |
|
||||
| Markdown | Renders markdown with syntax highlighting + diffs | v0.8.0 |
|
||||
| Dialog | Modal overlays with stack management | v0.9.0 |
|
||||
| Toast | Transient notifications (info/success/warning/error) | v0.9.0 |
|
||||
| Mouse | Event handlers, hit-testing, text selection | v0.10.0 |
|
||||
| Slot | Plugin system — named slots for extensible UI | v0.11.0 |
|
||||
|
||||
## Backend features
|
||||
|
||||
| Feature | modern | simple |
|
||||
|-------------------|--------|--------|
|
||||
| Truecolor (24-bit)| Yes | No |
|
||||
| Bold/italic | Yes | No |
|
||||
| OSC 8 hyperlinks | Yes | No |
|
||||
| DECICM sync | Yes | No |
|
||||
| SGR mouse | Yes | No |
|
||||
| Kitty keyboard | Yes | No |
|
||||
| Box drawing chars | Unicode| ASCII |
|
||||
| Pipe-safe | No | Yes |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
sbcl --script run-all-tests.lisp
|
||||
|
||||
# Tangle org files
|
||||
emacs --batch --eval "(progn (require 'org) (find-file \"org/FILE.org\") (org-babel-tangle) (kill-buffer))"
|
||||
```
|
||||
|
||||
Literate programming: `.org` files in `org/` are the source of truth.
|
||||
`.lisp` files are generated by tangling.
|
||||
|
||||
## License
|
||||
|
||||
TBD
|
||||
# Test
|
||||
|
||||
103
demo.lisp
103
demo.lisp
@@ -1,28 +1,79 @@
|
||||
;; demo.lisp — minimal cl-tty demo
|
||||
(load "/root/quicklisp/setup.lisp")
|
||||
(ql:quickload :fiveam :silent t)
|
||||
(load "backend/package.lisp")
|
||||
(load "backend/classes.lisp")
|
||||
(load "backend/simple.lisp")
|
||||
(load "backend/modern.lisp")
|
||||
(load "layout/layout.lisp")
|
||||
(load "src/components/package.lisp")
|
||||
(load "src/components/dirty.lisp")
|
||||
(load "src/components/box.lisp")
|
||||
(load "src/components/text.lisp")
|
||||
(load "src/components/render.lisp")
|
||||
(in-package :cl-tty.box)
|
||||
;;; demo.lisp — cl-tty demo application
|
||||
;;; Run: sbcl --script demo.lisp
|
||||
|
||||
;; Demo 1: Simple backend (ASCII)
|
||||
(let* ((b (make-simple-backend))
|
||||
(bx (make-box :border-style :rounded :title " Hello World " :width 30 :height 5)))
|
||||
(compute-layout (box-layout-node bx) 30 5)
|
||||
(render bx b))
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:register-local-projects)
|
||||
(ql:quickload :cl-tty :silent t)
|
||||
|
||||
;; Demo 2: Box with text inside
|
||||
(let* ((b (make-simple-backend))
|
||||
(tx (make-text "This is cl-tty in action!" :width 28 :height 1)))
|
||||
(setf (layout-node-direction (text-layout-node tx)) :column)
|
||||
(compute-layout (text-layout-node tx) 28 1)
|
||||
(render tx b)
|
||||
(format t "~%~%"))
|
||||
(in-package :cl-tty)
|
||||
|
||||
;; ─── Helper: write a string at (x, y) with optional styling ────────────────
|
||||
|
||||
(defun write-at (backend x y string &key fg bg bold)
|
||||
(let ((styled (if bold (format nil "~c[1m~a~c[0m" #\Esc string #\Esc) string)))
|
||||
(backend-write backend x y styled fg bg)))
|
||||
|
||||
;; ─── Demo ───────────────────────────────────────────────────────────────────
|
||||
|
||||
(defun run-demo ()
|
||||
(let* ((backend (make-instance 'cl-tty.backend:modern-backend))
|
||||
(w 80) (h 24))
|
||||
;; Initialize
|
||||
(initialize-backend backend)
|
||||
(clear-screen backend)
|
||||
(backend-write backend 0 0 (format nil "~c[?25l" #\Esc)) ; hide cursor
|
||||
|
||||
;; Title box
|
||||
(draw-border backend 1 1 78 3 :double :title " cl-tty Demo ")
|
||||
(write-at backend 3 2 "A pure-CL terminal UI framework. No ncurses, no FFI."
|
||||
:bold t)
|
||||
|
||||
;; Feature grid
|
||||
(draw-border backend 1 5 78 12 :single :title " Components ")
|
||||
(let ((items '((" Box Bordered containers with title and background"
|
||||
" Text Styled text with word-wrap and spans")
|
||||
(" ScrollBox Scrollable viewport with scrollbars"
|
||||
" TabBar Horizontal tab navigation")
|
||||
(" Select Dropdown with fuzzy filter"
|
||||
" TextInput / TextArea Single/multi-line input with undo")
|
||||
(" Markdown Renders markdown with syntax highlighting"
|
||||
" Dialog / Toast Modal overlays and notifications")
|
||||
(" Mouse Event handlers and text selection"
|
||||
" Slot System Named slots for extensible UI"))))
|
||||
(loop for i from 0 below 5
|
||||
for (col1 col2) = (nth i items)
|
||||
do (write-at backend 3 (+ 7 i) col1)
|
||||
(write-at backend 42 (+ 7 i) col2)))
|
||||
|
||||
;; Backend features table
|
||||
(draw-border backend 1 18 78 5 :single :title " Backend Support ")
|
||||
(write-at backend 3 20 "Feature" :bold t)
|
||||
(write-at backend 25 20 "modern" :bold t)
|
||||
(write-at backend 40 20 "simple" :bold t)
|
||||
(write-at backend 3 21 "Truecolor (24-bit)")
|
||||
(write-at backend 25 21 "yes")
|
||||
(write-at backend 40 21 "no")
|
||||
(write-at backend 3 22 "OSC 8 hyperlinks")
|
||||
(write-at backend 25 22 "yes")
|
||||
(write-at backend 40 22 "no")
|
||||
|
||||
;; Footer
|
||||
(write-at backend 1 24 " Press q to quit " :bold t :fg :white :bg :blue)
|
||||
(backend-write backend 0 0 (format nil "~c[?25h" #\Esc)) ; show cursor
|
||||
|
||||
;; Wait for q
|
||||
(loop
|
||||
(let ((ch (read-raw-byte :timeout 1)))
|
||||
(when ch
|
||||
(when (or (char= (code-char ch) #\q)
|
||||
(= ch 3)) ; Ctrl+C
|
||||
(return)))))
|
||||
|
||||
;; Cleanup
|
||||
(clear-screen backend)
|
||||
(shutdown-backend backend)))
|
||||
|
||||
;; ─── Run ────────────────────────────────────────────────────────────────────
|
||||
|
||||
(when (probe-file "/dev/tty")
|
||||
(run-demo))
|
||||
|
||||
@@ -577,6 +577,12 @@ slots. The component tree renders whatever is registered.
|
||||
All 11 phases integrated and tested. Applications can build rich terminal UIs
|
||||
from the component library without writing custom ncurses code.
|
||||
|
||||
** DONE Documentation
|
||||
- README.org with overview, architecture, component table, quick start
|
||||
- demo.lisp — working example exercising multiple components
|
||||
- run-all-tests.lisp — single-script test runner
|
||||
- Full test suite: ~280 checks, 100% passing across all 9 suites
|
||||
|
||||
* Neurosymbolic Phase Reference
|
||||
|
||||
| Phase | Component | Lines | Release |
|
||||
|
||||
41
run-all-tests.lisp
Normal file
41
run-all-tests.lisp
Normal file
@@ -0,0 +1,41 @@
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:register-local-projects)
|
||||
(ql:quickload :cl-tty :silent t)
|
||||
|
||||
;; Load all test files
|
||||
(dolist (f '("backend/tests.lisp" "backend/modern-tests.lisp"
|
||||
"layout/tests.lisp"
|
||||
"src/components/box-tests.lisp"
|
||||
"src/components/dirty-tests.lisp"
|
||||
"src/components/render-tests.lisp"
|
||||
"src/components/theme-tests.lisp"
|
||||
"src/components/input-tests.lisp"
|
||||
"tests/scrollbox-tabbar-tests.lisp"
|
||||
"tests/select-tests.lisp"
|
||||
"tests/markdown-tests.lisp"
|
||||
"tests/dialog-tests.lisp"
|
||||
"tests/mouse-tests.lisp"
|
||||
"tests/slot-tests.lisp"))
|
||||
(load f))
|
||||
|
||||
;; Run all test suites
|
||||
(dolist (suite '((:cl-tty-backend-test "BACKEND-SUITE")
|
||||
(:cl-tty-box-test "BOX-SUITE")
|
||||
(:cl-tty-input-test "INPUT-SUITE")
|
||||
(:cl-tty-scrollbox-test "SCROLLBOX-SUITE")
|
||||
(:cl-tty-select-test "SELECT-SUITE")
|
||||
(:cl-tty-markdown-test :cl-tty-markdown-test)
|
||||
(:cl-tty-dialog-test "DIALOG-SUITE")
|
||||
(:cl-tty-mouse-test "MOUSE-SUITE")
|
||||
(:cl-tty-slot-test "SLOT-SUITE")))
|
||||
(let* ((pkg (find-package (first suite)))
|
||||
(suite-name (second suite))
|
||||
(s (etypecase suite-name
|
||||
(keyword (find-symbol (string suite-name) pkg))
|
||||
(string (find-symbol suite-name pkg)))))
|
||||
(format t "~&=== ~a ===~%" (first suite))
|
||||
(if s
|
||||
(fiveam:explain! (fiveam:run s))
|
||||
(format t "Suite not found~%"))))
|
||||
|
||||
(uiop:quit 0)
|
||||
Reference in New Issue
Block a user