commit 3e0268c982909328e739106229ea578f79783bf4 Author: Amr Gharbeia Date: Mon May 11 06:48:20 2026 -0400 Initial commit: cl-tui — Reusable Common Lisp Terminal UI Framework Layout engine (Yoga FFI), renderables (Box, Text), rendering engine, theme engine, TextInput/Textarea, ScrollBox/TabBar, Select, Markdown/Code/Diff, dialog system, mouse support, plugin/slot system. diff --git a/README.org b/README.org new file mode 100644 index 0000000..642f104 --- /dev/null +++ b/README.org @@ -0,0 +1,52 @@ +#+TITLE: cl-tui — Reusable Common Lisp Terminal UI Framework +#+STARTUP: content +#+FILETAGS: :project:cl-tui:readme: + +* cl-tui + +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-tui 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-tui application runs remotely — same code, same components, +accessible from anywhere. + +** Architecture + +``` +Application code (any CL project) + └── cl-tui (layout, components, theme, events, dialogs) + └── Yoga (Flexbox layout — C library via FFI) + └── croatoan (ncurses terminal rendering) +``` + +cl-tui depends only on croatoan and Yoga. It is not tied to any application. + +** Dependencies + +- 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) + +** Status + +v0.1.0 — Layout engine (in progress) + +See ~docs/ROADMAP.org~ for the full release plan. + +** License + +TBD diff --git a/docs/ROADMAP.org b/docs/ROADMAP.org new file mode 100644 index 0000000..1d1f0e5 --- /dev/null +++ b/docs/ROADMAP.org @@ -0,0 +1,513 @@ +#+TITLE: cl-tui Roadmap +#+STARTUP: content +#+FILETAGS: :docs:roadmap:cl-tui: + +* The Roadmap + +Each phase is one minor release. Phases ship in dependency order — each depends on +the components from prior phases. The layout engine ships first because everything +else builds on it. + +Feature releases increment the minor version (v0.X.0). Bugfix releases increment +the patch version (v0.X.Y). + +** File Update Checklist + +When a version ships: +1. ~ROADMAP.org~ — mark item DONE, update LOGBOOK timestamp +2. ~README.org~ — update Status line +3. ~cl-tui.asd~ — update version string + +** v0.1.0: Layout Engine + +Yoga Flexbox backend wrapped in a Common Lisp API. This is the foundation — +every component after v0.1.0 uses the layout engine for positioning. + +*** TODO Yoga FFI binding +:PROPERTIES: +:ID: id-v010-yoga-ffi +:CREATED: [2026-05-10 Sat] +:END: + +- Load the Yoga shared library via CFFI +- Define foreign types for ~YGNodeRef~, ~YGSize~, ~YGValue~, ~YGDirection~, ~YGFlexDirection~, ~YGAlign~, ~YGJustify~, ~YGWrap~, ~YGPositionType~, ~YGOverflow~, ~YGDisplay~, ~YGEdge~ +- Bind core functions: ~node-new~, ~node-free~, ~node-style-set-*~, ~node-layout-get-*~, ~calculate-layout~ +- ~100 lines CFFI + +*** TODO Layout primitives +:PROPERTIES: +:ID: id-v010-layout-primitives +:CREATED: [2026-05-10 Sat] +:END: + +- ~(make-layout-node)~ — wraps a ~YGNodeRef~ in a CLOS object +- ~(layout-node-set-dimension node width height)~ — sets width/height in points +- ~(layout-node-set-flex node &key grow shrink basis)~ — flex properties +- ~(layout-node-set-direction node :row | :column | :row-reverse | :column-reverse)~ +- ~(layout-node-set-wrap node :nowrap | :wrap | :wrap-reverse)~ +- ~(layout-node-set-align node :flex-start | :center | :flex-end | :stretch | :baseline)~ +- ~(layout-node-set-justify node :flex-start | :center | :flex-end | :space-between | :space-around | :space-evenly)~ +- ~(layout-node-set-padding node &key top right bottom left x y)~ +- ~(layout-node-set-margin node &key top right bottom left x y)~ +- ~(layout-node-set-gap node &key row column)~ +- ~(layout-node-set-position node :relative | :absolute &key top right bottom left)~ +- ~(layout-node-set-border node width)~ +- ~(layout-node-add-child parent child)~ — builds the tree +- ~(layout-calculate root width height)~ — runs Yoga's calculateLayout, populates each node's computed x/y/w/h +- ~200 lines CL + +*** TODO Layout composable API +:PROPERTIES: +:ID: id-v010-layout-composable +:CREATED: [2026-05-10 Sat] +:END: + +Convenience macros to build layout trees from CL function calls: + +- ~(vbox &key ... children ...)~ → column-direction container with children +- ~(hbox &key ... children ...)~ → row-direction container with children +- ~(overlay base child)~ — absolute-positioned overlay over a relative base +- ~(spacer &key grow)~ — empty flex spacer +- ~(layout-render root parent-window)~ — computes layout then walks the tree, calling each child's render function with its computed x, y, w, h +- ~50 lines CL macros + +~350 lines total. Dependencies: Yoga shared library, CFFI, croatoan. + +*** FiveAM tests +- ~test-layout-basic~ — vbox with two children computes correct y positions +- ~test-layout-hbox~ — hbox with two children computes correct x positions +- ~test-layout-flex~ — flex-grow distributes space correctly +- ~test-layout-absolute~ — absolute child positions relative to parent +- ~test-layout-nested~ — nested vbox/hbox produces correct leaf positions + +** v0.2.0: Renderables — Box and Text + +The first two renderable types that every application uses. A Box draws borders +and backgrounds. A Text renders strings with color and style. Together they +cover 80% of terminal UI. + +*** TODO Box renderable +:PROPERTIES: +:ID: id-v020-box +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass box ...)~ — renderable with background color, border, title +- ~(render-box box window)~ — draws border (single/double/rounded), fills background, renders title +- Border styles: ~:single~, ~:double~, ~:rounded~ +- Title alignment: ~:left~, ~:center~, ~:right~ +- ~:focusable~ property — renders focused border color when focused +- ~100 lines + +*** TODO Text renderable +:PROPERTIES: +:ID: id-v020-text +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass text ...)~ — renderable with content, fg/bg color, wrap mode +- ~(render-text text window)~ — renders text at the layout position, wraps at width +- Word-wrap: ~:none~ (truncate) or ~:word~ (break at word boundaries) +- CJK/emoji character-width aware wrapping +- ~100 lines + +*** TODO Inline text styles +:PROPERTIES: +:ID: id-v020-inline +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass span ...)~ — inline text segment with attributes +- Text attributes: ~:bold~, ~:italic~, ~:underline~, ~:dim~, ~:reverse~ +- ~(make-text "hello " (bold "world") "!")~ — builds styled text from spans and strings +- ~60 lines + +*** TODO Dirty tracking +:PROPERTIES: +:ID: id-v020-dirty +:CREATED: [2026-05-10 Sat] +:END: + +- ~(mark-dirty component)~ — flags component and all ancestors +- ~(dirty-p component)~ — returns T if the component needs re-rendering +- ~(mark-clean component)~ — clears dirty flag after render +- ~40 lines + +~300 lines total. Dependencies: Phase 1 (layout engine). + +** v0.3.0: Rendering Engine + +The pipeline that goes from component tree to terminal output. Handles dirty +propagation, incremental rendering (only dirty branches), scissor clipping, +and diff-based output. + +*** TODO Component tree → render commands +:PROPERTIES: +:ID: id-v030-pipeline +:CREATED: [2026-05-10 Sat] +:END: + +- ~(render-screen root screen)~ — entry point: computes layout, walks dirty branches, collects render commands +- Render commands are lists: ~(:box x y w h bg border title)~, ~(:text x y str fg bg attrs)~ +- Each component's ~render~ function returns a list of render commands +- ~100 lines + +*** TODO Scissor clipping +:PROPERTIES: +:ID: id-v030-scissor +:CREATED: [2026-05-10 Sat] +:END: + +- ~(with-scissor (window x y w h) &body body)~ — clips all render operations to a rectangle +- Pushes/pops scissor state so nested containers clip correctly +- ~50 lines + +*** TODO Incremental diff output +:PROPERTIES: +:ID: id-v030-diff-output +:CREATED: [2026-05-10 Sat] +:END: + +- ~*framebuffer*~ — a 2D array of (char, fg-color, bg-color, attrs) tuples +- ~(flush-framebuffer screen)~ — compares framebuffer to previous frame, writes only changed cells via croatoan +- ~(clear-dirty screen)~ — clears all dirty flags after a successful flush +- Croatoan compatibility: uses ~add-string~ for unchanged text, ~clear~ + ~add-string~ for changed regions +- ~150 lines + +~300 lines total. Dependencies: Phase 2 (renderables + dirty tracking). + +** v0.4.0: Theme Engine + +Semantic color tokens, dark/light variants, hex → truecolor resolution, and +built-in presets. Application code references semantic roles (~:error~, ~:accent~), +never hex values. + +*** TODO Semantic color tokens +:PROPERTIES: +:ID: id-v040-tokens +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass theme ...)~ — holds a mapping from semantic roles to hex colors +- 30+ semantic roles: ~:primary~, ~:secondary~, ~:accent~, ~:error~, ~:warning~, ~:success~, ~:info~, ~:text~, ~:text-muted~, ~:background~, ~:background-panel~, ~:background-element~, ~:border~, ~:border-active~, ~:diff-added~, ~:diff-removed~, ~:diff-context~, ~:markdown-heading~, ~:markdown-code~, ~:markdown-link~, ~:markdown-quote~, ~:syntax-keyword~, ~:syntax-function~, ~:syntax-string~, ~:syntax-number~, ~:syntax-comment~, ~:syntax-type~ +- ~120 lines + +*** TODO theme-color +:PROPERTIES: +:ID: id-v040-theme-color +:CREATED: [2026-05-10 Sat] +:END: + +- ~(theme-color theme role)~ → returns the croatoan color pair number for the role +- ~(themed-add-string window x y str :color :error)~ — renders text with a theme semantic role +- Color pair caching: resolve hex → croatoan ~init-color~ once per (fg, bg) pair, reuse +- ~40 lines + +*** TODO Built-in presets +:PROPERTIES: +:ID: id-v040-presets +:CREATED: [2026-05-10 Sat] +:END: + +8 presets: default (gold), professional, minimal, nord, tokyonight, catppuccin, monokai, gruvbox +- Each preset is a plist: ~(:primary "#FFD700" :error "#BF616A" ...)~ +- ~(theme-load :nord)~ — activates a preset, re-renders dirty +- Load from ~/.config/cl-tui/themes/.lisp~ for custom themes +- ~80 lines + +*** TODO Dark/light variants +:PROPERTIES: +:ID: id-v040-dark-light +:CREATED: [2026-05-10 Sat] +:END: + +- Each preset defines both ~:dark~ and ~:light~ variants +- ~(theme-set-mode :dark | :light)~ — switches variant +- Auto-detect: read terminal background color (croatoan's background), pick closest variant +- ~50 lines + +~290 lines total. Dependencies: Phase 2 (renderables), Croatoan's ~init-color~/~color-pair~. + +** v0.5.0: Text Input + Keybinding System + +Text input widgets with readline/emacs keybindings. A layered keybinding system +that routes keystrokes through global → local → input layers. + +*** TODO TextInput — single-line input +:PROPERTIES: +:ID: id-v050-textinput +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass text-input ...)~ — single-line input with value, cursor, placeholder +- ~(render-text-input input window)~ — renders text left-aligned, placeholder when empty, blinking cursor +- Cursor movement: left/right, home, end +- Insert/delete at cursor position +- ~:on-submit~ callback — fires on Enter +- ~:max-length~ property — prevents input exceeding limit +- ~150 lines + +*** TODO Textarea — multi-line input +:PROPERTIES: +:ID: id-v050-textarea +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass textarea ...)~ — multi-line input with value, cursor (row, column), selection +- ~(render-textarea area window)~ — renders visible lines, cursor, selection highlight +- Cursor: up/down, left/right, word-forward/backward, line/home/end, buffer/home/end +- Selection: Shift + navigation extends selection +- Undo/redo stack (configurable depth, default 100) +- ~:on-submit~ callback — fires on Enter +- ~200 lines + +*** TODO Keybinding system +:PROPERTIES: +:ID: id-v050-keybindings +:CREATED: [2026-05-10 Sat] +:END: + +- Layered keymaps: ~:global~ → ~:local~ → ~:input~ (input layer takes priority when text input is focused) +- ~(defkeymap :global '((:ctrl+p . command-palette) (:ctrl+c,ctrl+d . quit)))~ +- Key format: ~:ctrl+p~, ~:alt+f~, ~:shift+tab~, ~(:ctrl+c :ctrl+d)~ (chord) +- Chord sequences: first key starts a timer, second key within timeout dispatches +- ~:leader~ key (default ~Ctrl+X~) with configurable timeout +- Key names normalized from croatoan's ~:code-key~ + ~:key-name~ output +- ~150 lines + +~500 lines total. Dependencies: Phase 3 (rendering engine), Phase 4 (theme). + +** v0.6.0: ScrollBox + TabBar + +Container components. ScrollBox handles content larger than the viewport. +TabBar handles horizontal tab navigation. + +*** TODO ScrollBox +:PROPERTIES: +:ID: id-v060-scrollbox +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass scroll-box ...)~ — container with vertical/horizontal scroll +- Viewport culling: only render children whose y position is within the visible range +- Scroll offset: ~:scroll-y~, ~:scroll-x~ slots +- ScrollBy: PageUp/PageDown (viewport height), Up/Down (1 line), Home/End (buffer start/end) +- Scrollbars: vertical and horizontal (single-line, rendered with block characters) +- Sticky scroll: when scrolled to bottom and new content arrives, auto-scroll to show it. When user scrolls up, stop auto-scrolling until they scroll back down. +- ~200 lines + +*** TODO TabBar +:PROPERTIES: +:ID: id-v060-tabbar +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass tab-bar ...)~ — horizontal row of tabs +- ~(tab-bar-add tab-bar id title &optional content)~ +- ~:active-tab~ slot — only renders content for the active tab +- Tab rendering: highlighted active tab, dim inactive tabs +- Left/Right or Ctrl+PageUp/PageDn to navigate tabs +- ~100 lines + +~300 lines total. Dependencies: Phase 3 (rendering engine), Phase 4 (theme). + +** v0.7.0: Select — Dropdown + Fuzzy Filter + +A selection list component — the building block for command palettes, theme +pickers, agent selectors, file pickers. + +*** TODO Select +:PROPERTIES: +:ID: id-v070-select +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass select ...)~ — list of options with keyboard navigation +- ~:options~ — list of plists: ~((:title "Nord" :value :nord :category "Themes") ...)~ +- Categories: options can be grouped. Category headers rendered dim, non-selectable +- Up/Down/Ctrl+P/Ctrl+N to navigate, Enter to select, Esc to dismiss +- ~:on-select~ callback — fires on Enter +- ~:filter~ property — when set, filters the option list. Options whose title contains the filter (case-insensitive) are shown. +- Fuzzy filter: when ~:filter~ is non-nil and no exact matches, uses trigram-based fuzzy matching (3-character sliding window Jaccard similarity) +- ~150 lines + +~150 lines total. Dependencies: Phase 5 (keybindings), Phase 4 (theme). + +** v0.8.0: Markdown + Code + Diff Rendering + +Content rendering components. Markdown for agent responses. Code for syntax +highlighting. Diff for file changes. + +*** TODO Markdown +:PROPERTIES: +:ID: id-v080-markdown +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass markdown ...)~ — renders markdown content as styled text +- Heading levels 1-6: colored by theme (~:markdown-heading~) with level-based sizing +- Bold, italic, inline code, strikethrough — rendered as croatoan text attributes +- Code blocks: fenced (~```~) and indented. Background-colored, syntax-highlighted via regex +- Links: OSC 8 hyperlinks (clickable in Kitty, WezTerm, iTerm2, Ghostty). Format: ~\x1b]8;;url\x1b\\...link text...\x1b]8;;\x1b\\~ +- Blockquotes: colored left border (~:markdown-quote~), indented text +- Tables: aligned column text, no borders. Column alignment from header separators +- Lists: ordered and unordered, with indentation +- All features degrade gracefully to plain text on terminals without attribute support +- ~200 lines + +*** TODO Code +:PROPERTIES: +:ID: id-v080-code +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass code ...)~ — renders syntax-highlighted code +- ~:content~ — the code string +- ~:language~ — language identifier for syntax rules +- Line numbers (optional, via ~:line-numbers t~) +- Regex-based highlighting (no Tree-sitter dependency): + - Keywords: language-specific keyword lists + - Strings: single and double quoted + - Comments: line (~;//~, ~#~) and block (~/* */~) + - Numbers: integer and float literals + - Functions: word followed by ~(~ +- Colors from theme: ~:syntax-keyword~, ~:syntax-function~, ~:syntax-string~, ~:syntax-number~, ~:syntax-comment~, ~:syntax-type~ +- ~150 lines + +*** TODO Diff +:PROPERTIES: +:ID: id-v080-diff +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass diff ...)~ — renders unified diff output +- ~:content~ — diff text (standard unified diff format) +- Added lines: ~+~ prefix, green background (~:diff-added~) +- Removed lines: ~-~ prefix, red background (~:diff-removed~) +- Context lines: ~ ~ prefix, neutral background (~:diff-context~) +- Line numbers: optional, rendered in ~:diff-line-number~ color +- ~50 lines + +~400 lines total. Dependencies: Phase 4 (theme), Phase 2 (renderables). + +** v0.9.0: Dialog System + Toast + +Modal overlays and transient notifications. + +*** TODO Dialog base +:PROPERTIES: +:ID: id-v090-dialog +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defclass dialog ...)~ — absolute-positioned overlay with backdrop +- Backdrop: semi-transparent (dimmed background color) +- Centered panel with ~:background-panel~ color, border +- ~:on-dismiss~ callback — fires on Esc or backdrop click +- ~:size~ — ~:small~ (40 cols), ~:medium~ (60 cols), ~:large~ (88 cols). Height computed from content. +- Stack-based: dialogs push/pop on a ~*dialog-stack*~ +- Esc dismisses top dialog. Ctrl+C clears stack. +- ~100 lines + +*** TODO Dialog sub-classes +:PROPERTIES: +:ID: id-v090-dialog-types +:CREATED: [2026-05-10 Sat] +:END: + +- ~alert-dialog~ — title + message + OK button +- ~confirm-dialog~ — title + message + Yes/No/Cancel buttons +- ~select-dialog~ — wraps a Select component in a modal. Title, searchable list, action buttons +- ~prompt-dialog~ — wraps a TextInput in a modal. Title, input, OK/Cancel buttons +- ~60 lines + +*** TODO Toast notifications +:PROPERTIES: +:ID: id-v090-toast +:CREATED: [2026-05-10 Sat] +:END: + +- ~(toast title &key variant duration)~ — shows a transient notification +- Variants: ~:info~ (blue), ~:success~ (green), ~:warning~ (yellow), ~:error~ (red) — colored left border +- ~:duration~ — auto-dismiss after N milliseconds (default 5000) +- Position: top-right corner, max 60 cols wide +- Multiple toasts stack vertically +- ~60 lines + +~220 lines total. Dependencies: Phase 3 (rendering engine), Phase 4 (theme), Phase 5 (TextInput), Phase 7 (Select). + +** v0.10.0: Mouse Support + +Mouse event propagation through the component tree. + +*** TODO Mouse events +:PROPERTIES: +:ID: id-v100-mouse +:CREATED: [2026-05-10 Sat] +:END: + +- Enable croatoan mouse mode: ~(setf (mouse-enabled-p window) t)~ +- Parse ncurses mouse codes: button (left/right/middle), state (press/release/drag), x, y +- Ctrl/Shift/Meta modifiers from mouse event +- ~:on-mouse-down~, ~:on-mouse-up~, ~:on-mouse-move~, ~:on-mouse-scroll~ callbacks on components +- Hit-testing: walk the component tree from root, find the deepest component whose rect contains (x, y) +- Event propagation: component consumes event by returning T from callback; otherwise bubbles to parent +- Scroll wheel: mapped to PageUp/PageDown in ScrollBox +- Click on OSC 8 link: extract URL, open via ~xdg-open~ +- ~100 lines + +*** TODO Text selection + copy +:PROPERTIES: +:ID: id-v100-selection +:CREATED: [2026-05-10 Sat] +:END: + +- Mouse drag: highlight text between drag start and current position +- ~(get-selection)~ — returns the selected text as a string +- Copy: pipe selection to ~xclip~ / ~wl-copy~ / ~pbcopy~ +- ~50 lines + +~150 lines total. Dependencies: Phase 3 (rendering engine). + +** v0.11.0: Plugin / Slot System + +Extensible named slots. Applications and plugins register content into named +slots. The component tree renders whatever is registered. + +*** TODO Slot system +:PROPERTIES: +:ID: id-v110-slots +:CREATED: [2026-05-10 Sat] +:END: + +- ~(defslot :sidebar-title &key order render-fn)~ — registers a rendering function for a slot +- ~(slot-render slot-name ...)~ — calls all registered render-fns for the slot in priority-ordered sequence +- Slot modes: ~:stack~ (render all, default), ~:replace~ (last registered wins), ~:single-winner~ (first matching wins) +- ~:order~ integer — sorting key for ~:stack~ mode (lower = renders first) +- Built-in slot naming convention: component name, then sub-slot: ~sidebar-title~, ~sidebar-content~, ~home-logo~, ~home-prompt~ +- ~100 lines + +~100 lines total. Dependencies: Phase 2 (renderables + layout). + +* v1.0.0: Complete Framework + +All 11 phases integrated and tested. Applications can build rich terminal UIs +from the component library without writing custom ncurses code. + +* Neurosymbolic Phase Reference + +| Phase | Component | Lines | Release | +|-------+------------------------------------+--------+---------| +| 1 | Layout engine (Yoga FFI + API) | ~350 | v0.1.0 | +| 2 | Renderables (Box, Text) + dirty | ~300 | v0.2.0 | +| 3 | Rendering engine (diff, scissor) | ~300 | v0.3.0 | +| 4 | Theme engine (tokens, presets) | ~290 | v0.4.0 | +| 5 | TextInput + Textarea + keybindings | ~500 | v0.5.0 | +| 6 | ScrollBox + TabBar | ~300 | v0.6.0 | +| 7 | Select (dropdown + fuzzy filter) | ~150 | v0.7.0 | +| 8 | Markdown + Code + Diff | ~400 | v0.8.0 | +| 9 | Dialog system + Toast | ~220 | v0.9.0 | +| 10 | Mouse support + selection | ~150 | v0.10.0 | +| 11 | Plugin / slot system | ~100 | v0.11.0 | +|-------+------------------------------------+--------+---------| +| Total | | ~3060 | |