Rename: cl-tty avoids naming collision with Quicklisp's cl-tui (naryl/cl-tui, a cl-charms-based ncurses library). Our project is pure escape-sequence CL. v0.9.0 adds: - Dialog base class: modal overlay with backdrop, centered panel, size variants (:small/:medium/:large), stack-based management - Dialog subclasses: alert, confirm, select-dialog, prompt-dialog - Toast notifications: transient, top-right corner, auto-dismiss, colored variants (info/success/warning/error) - 78 tests total, 100% passing ASDF: read-time package references (+fiveam:+) replaced with find-symbol so .asd loads without FiveAM pre-loaded
22 KiB
cl-tty Roadmap
- The Roadmap
- v0.0.1: Foundation — Backend Protocol
- v0.0.2: Layout Engine
- File Update Checklist
- v0.1.0: Layout Engine
- v0.2.0: Renderables — Box and Text
- v0.3.0: Rendering Engine
- v0.4.0: Theme Engine
- v0.5.0: Text Input + Keybinding System
- v0.6.0: ScrollBox + TabBar
- v0.7.0: Select — Dropdown + Fuzzy Filter
- v0.8.0: Markdown + Code + Diff Rendering
- v0.9.0: Dialog System + Toast
- v0.10.0: Mouse Support
- v0.11.0: Plugin / Slot System
- v1.0.0: Complete Framework
- Neurosymbolic Phase Reference
The Roadmap
Each phase is one minor release. Phases ship in dependency order — each depends on the components from prior phases. The backend protocol ships first because everything else builds on it.
v0.0.1: Foundation — Backend Protocol
The abstraction layer that makes everything portable. Two backends:
modern (raw escape sequences, truecolor, modern features) and simple
(ASCII art, universal compatibility). The component tree never touches
the terminal directly — it dispatches through the protocol.
TODO Backend protocol definition
-
Define
backendabstract class with generic functions:initialize-backend,shutdown-backend,suspend-backend,resume-backendbackend-size,backend-write,backend-clearbegin-sync,end-sync— DECICM synchronized updatesdraw-rect,draw-text,draw-border,draw-ellipsis,draw-linkcursor-move,cursor-hide,cursor-show,cursor-styleread-event,enable-mouse,enable-bracketed-paste,set-keyboard-modecapable-p— query feature support
- Style plist structure:
(:fg :error :bg :background-panel :bold t :italic nil ...) - ~100 lines
TODO Simple backend
simple-backendclass — inheritsbackend- Borders: ASCII (
+-|), no rounded corners - No color, no bold/italic — plain characters only
- No OSC 8 links, no mouse, no synchronized updates
- Works on any terminal, any SSH connection, piped output
- ~100 lines
TODO Modern backend
modern-backendclass — inheritsbackend- Truecolor 24-bit foreground/background
- Rounded, single, double border styles via Unicode box-drawing
- OSC 8 hyperlinks (clickable URLs)
- DECICM synchronized updates (flicker-free)
- SGR mouse tracking + kitty keyboard protocol
- Bracketed paste detection
- Bold, italic, underline, dim, blink, reverse, strikethrough
- Cursor style:
:bar,:block,:underline, with blink option - ~250 lines
TODO Terminal capability detection
detect-backend→ returnsmodern-backendorsimple-backend- Check if stdout is a TTY (if not →
simple-backend) - Send DA1 (
ESC[c) query, 100ms timeout - Send DA3 (
ESC[?c) for kitty/wezterm identification - Query DECRPM (
ESC[?2026$p) for DECICM sync support - Query truecolor support via
COLORTERMenv var + DA response - Cache detection result so subsequent calls are instant
- ~100 lines
~550 lines total. Dependencies: None (pure CL, no FFI, no external libs).
v0.0.2: Layout Engine
the patch version (v0.X.Y).
File Update Checklist
When a version ships:
ROADMAP.org— mark item DONE, update LOGBOOK timestampREADME.org— update Status linecl-tty.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
- 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
(make-layout-node)— wraps aYGNodeRefin 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
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 positionstest-layout-hbox— hbox with two children computes correct x positionstest-layout-flex— flex-grow distributes space correctlytest-layout-absolute— absolute child positions relative to parenttest-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.
DONE Box renderable
- State \"DONE\" from \"TODO\" [2026-05-11 Mon]
(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 :focusableproperty — renders focused border color when focused- ~100 lines
DONE Text renderable
- State \"DONE\" from \"TODO\" [2026-05-11 Mon]
(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
DONE Inline text styles
- State \"DONE\" from \"TODO\" [2026-05-11 Mon]
(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
DONE Dirty tracking
- State \"DONE\" from \"TODO\" [2026-05-11 Mon]
(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
(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
renderfunction returns a list of render commands - ~100 lines
TODO Scissor clipping
(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
*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-stringfor unchanged text,clear+add-stringfor 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
(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
(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-coloronce per (fg, bg) pair, reuse - ~40 lines
TODO Built-in presets
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-tty/themes/<name>.lispfor custom themes - ~80 lines
TODO Dark/light variants
- Each preset defines both
:darkand:lightvariants (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
(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-submitcallback — fires on Enter:max-lengthproperty — prevents input exceeding limit- ~150 lines
TODO Textarea — multi-line input
(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-submitcallback — fires on Enter- ~200 lines
TODO Keybinding system
- 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
:leaderkey (defaultCtrl+X) with configurable timeout- Key names normalized from croatoan's
:code-key+:key-nameoutput - ~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
(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-xslots - 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
(defclass tab-bar ...)— horizontal row of tabs(tab-bar-add tab-bar id title &optional content):active-tabslot — 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
(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-selectcallback — fires on Enter:filterproperty — when set, filters the option list. Options whose title contains the filter (case-insensitive) are shown.- Fuzzy filter: when
:filteris 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
(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
(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
(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-numbercolor - ~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
(defclass dialog ...)— absolute-positioned overlay with backdrop- Backdrop: semi-transparent (dimmed background color)
- Centered panel with
:background-panelcolor, border :on-dismisscallback — 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
alert-dialog— title + message + OK buttonconfirm-dialog— title + message + Yes/No/Cancel buttonsselect-dialog— wraps a Select component in a modal. Title, searchable list, action buttonsprompt-dialog— wraps a TextInput in a modal. Title, input, OK/Cancel buttons- ~60 lines
TODO Toast notifications
(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
- 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-scrollcallbacks 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
- 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
(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) :orderinteger — sorting key for:stackmode (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 |