From 825980b93bf4a3482a5f3a84e512fc3c3ae449e7 Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 11 May 2026 20:47:47 +0000 Subject: [PATCH] v1.0.0: Complete framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.org with overview, architecture, component table, quick start - demo.lisp — working TUI demo exercising multiple components - run-all-tests.lisp — single-script test runner - ROADMAP updated with v1.0.0 documentation milestone - Full test suite: ~280 checks, 100% passing across 9 suites --- README.org | 103 +++++++++++++++++++++++++++------------------ demo.lisp | 103 +++++++++++++++++++++++++++++++++------------ docs/ROADMAP.org | 6 +++ run-all-tests.lisp | 41 ++++++++++++++++++ 4 files changed, 186 insertions(+), 67 deletions(-) create mode 100644 run-all-tests.lisp diff --git a/README.org b/README.org index 2d9792b..d7f71c4 100644 --- a/README.org +++ b/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 diff --git a/demo.lisp b/demo.lisp index 4477a00..4904bdf 100644 --- a/demo.lisp +++ b/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)) diff --git a/docs/ROADMAP.org b/docs/ROADMAP.org index c91f725..829973b 100644 --- a/docs/ROADMAP.org +++ b/docs/ROADMAP.org @@ -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 | diff --git a/run-all-tests.lisp b/run-all-tests.lisp new file mode 100644 index 0000000..e9be87c --- /dev/null +++ b/run-all-tests.lisp @@ -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)