- Added missing closing paren for defun on-key in org/channel-tui-main.org line 616 (was 7 trailing ), now 8) - Replaced #\) character literal with (code-char 41) to avoid reader ambiguity with paren-delimiter counting - All 3 TUI org files tangled and verified compilable - passepartout/tui loads without errors under SBCL 2.5.2
9.3 KiB
cl-tty TUI Migration Plan
- Current Architecture (Croatoan)
- Migration Strategy: Option C (Hybrid)
- Step-by-step Plan
- What cl-tty Gains From This
- Files to Modify
- Verification
Croatoan is not working and passepartout's TUI needs a reliable rendering backend. cl-tty was built for exactly this use case. This plan details the migration from Croatoan (ncurses via CFFI) to cl-tty (pure CL, no FFI).
Current Architecture (Croatoan)
3 org files, ~2K LOC total:
- state.org (191 lines): state plist, theme presets, event queue, helpers
- main.org (1370 lines): key dispatch, daemon protocol, main loop
- view.org (480 lines): render functions, markdown rendering, gate trace
Croatoan API calls used:
| Croatoan call | Purpose | Count |
|---|---|---|
with-screen |
Terminal init (raw, no echo) | 1 |
make-instance 'window |
Window creation for layout | ~10 |
add-string |
Render text w/ fg, bg, attrs | ~20 |
get-char |
Read keypress | 1 |
code-key~/~key-name |
Convert raw code → keyword | 2 |
clear |
Clear window contents | 3 |
refresh |
Flush window to terminal | ~8 |
box |
Draw border around window | 2 |
width~/~height |
Query window dimensions | ~6 |
(setf cursor-position) |
Set cursor location | 1 |
function-keys-enabled-p |
Enable function key codes | 2 |
input-blocking |
Non-blocking input mode | 2 |
Migration Strategy: Option C (Hybrid)
Replace the rendering backend only. Keep passepartout's application logic (state machine, event handlers, daemon protocol, markdown parser) intact.
Don't rewrite the event handling into cl-tty's component/keymap system. Don't replace the state plist with cl-tty components. Replace Croatoan window operations with cl-tty backend primitives.
Why not pure component tree (Option B):
The 1370-line event handler in main.org is deeply coupled to the plist state model. Untangling it into cl-tty component event handlers would be churn with no user-visible benefit. The markdown renderer, gate trace, search mode, HITL panels, streaming text, and undo/redo are all app-specific logic that cl-tty doesn't need to know about. Keep them as-is, just swap the output path.
Step-by-step Plan
Step 1: Add cl-tty dependency (5 min)
- Add
:cl-ttytopassepartout/tuisystem dependencies in .asd - Remove
:croatoandependency - Add cl-tty to Quicklisp/local-projects or install path
Step 2: Replace with-screen with cl-tty init (30 min)
Replace:
(with-screen (scr :input-blocking nil :input-echoing nil :cursor-visible nil)
...)
With:
(sb-posix:with-raw-terminal
(let* ((be (cl-tty.backend:detect-backend))
(w (nth-value 0 (cl-tty.backend:backend-size be)))
(h (nth-value 1 (cl-tty.backend:backend-size be))))
(cl-tty.backend:initialize-backend be)
(unwind-protect
(tui-loop be w h)
(cl-tty.backend:shutdown-backend be))))
Step 3: Replace windows with cl-tty rendering (view.org, 2-3 hours)
Replace Croatoan window operations in view-status, view-chat, view-input:
add-string → cl-tty.backend:draw-text
box → cl-tty.backend:draw-border
clear → framebuffer clear or cl-tty.backend:backend-clear
refresh → framebuffer flush (flush-framebuffer)
Each render function (view-status, view-chat, view-input) takes:
- cl-tty backend instance (instead of Croatoan window)
- x/y/w/h region (instead of
width~/~heighton window)
Step 4: Wire framebuffer diffing (view.org + main.org, 1 hour)
Replace per-window clear~+~refresh with cl-tty's framebuffer:
- Create framebuffer at terminal size
- Each render function draws render commands into the framebuffer
- Main loop calls
flush-framebufferwhich diffs and writes only changed cells
The existing dirty-flag system ((st :dirty) as (list status chat input))
maps naturally: each dirty flag maps to which regions of the framebuffer
need rebuilding.
Step 5: Replace input handling (main.org, 1 hour)
Replace get-char + code-key~/~key-name conversion with cl-tty.input:read-event:
read-eventreturns structuredkey-eventstructs with:keyand:modifiers- No manual integer → keyword conversion needed
- Arrow keys, Enter, Backspace, Tab, PageUp/Down all come as keywords
- Ctrl+letter codes come as
(make-key-event :key 'a :ctrl t)
Key mapping table:
| Croatoan code | Current convert | cl-tty event |
|---|---|---|
| 263/127/8 | :backspace | (key :backspace) |
| 259 | :up | (key :up) |
| 258 | :down | (key :down) |
| 260 | :left | (key :left) |
| 261 | :right | (key :right) |
| 339 | :ppage | (key :page-up) |
| 338 | :npage | (key :page-down) |
| 13/10 | :enter | (key :enter) |
| 9 | :tab | (key :tab) |
| 27 | 27 | (key :escape) |
| 410 | KEY_RESIZE | (needs signal handler) |
| 21 (C-u) | 21 | (key #\u :ctrl t) |
| 1 (C-a) | 1 | (key #\a :ctrl t) |
| 5 (C-e) | 5 | (key #\e :ctrl t) |
Replace the cond dispatcher in on-key: change integer checks to keyword
comparisons. The logic stays identical — only the key representation changes.
Step 6: Handle SIGWINCH (main.org, 30 min)
cl-tty doesn't have built-in resize handling. Add a sb-sys:with-deadline
or SIGWINCH handler that sets a resize-pending flag. The main loop checks
this flag and calls backend-size to get new dimensions, then marks all
dirty flags.
Add to init-state:
:resize-pending nil
Add a SIGWINCH handler:
(sb-sys:enable-interrupt sb-posix:sigwinch
(lambda () (setf (st :resize-pending) t)))
In the main loop, check before rendering:
(when (st :resize-pending)
(setf (st :resize-pending) nil)
(multiple-value-setq (w h) (cl-tty.backend:backend-size be))
(setf (st :dirty) (list t t t)))
Step 7: Tone down to 10fps (main.org, 5 min)
The current 30fps ((sleep 0.03)) is overkill for a chat UI. Change to
(sleep 0.1) for 10fps. The framebuffer only sends changes — idle frames
cost nothing.
Step 8: Map theme colors (state.org, 30 min)
passepartout has 27 semantic theme keys. Croatoan uses keyword colors
(:green, :red, :cyan, :yellow, :magenta, :blue, :white,
:black) while cl-tty uses hex strings ("#00FF00") for truecolor or
named colors.
Solution: keep passepartout's *tui-theme* plist as-is. Change
theme-color to return hex strings compatible with cl-tty:
(defun theme-color-to-hex (role)
(let ((val (getf *tui-theme* role)))
(cond
((stringp val) val) ; already hex like "#ebdbb2"
((keywordp val) ; named Croatoan color → hex
(case val
(:green "#00FF00") (:red "#FF0000") (:cyan "#00FFFF")
(:yellow "#FFFF00") (:magenta "#FF00FF") (:blue "#0000FF")
(:white "#FFFFFF") (:black "#000000")
(t "#FFFFFF"))))))
The gruvbox and solarized presets already use hex strings — they work directly with cl-tty. Only the dark and light presets use Croatoan keywords and need mapping.
Step 9: Remove Croatoan TUI system (5 min)
The passepartout/tui system no longer needs :croatoan. Update the
ASDF definition.
What cl-tty Gains From This
This is the litmus test for cl-tty. If it can serve as the rendering backend for a real application, it validates the architecture. Specific needs that would drive cl-tty improvements:
- SIGWINCH handling — cl-tty should provide a
with-resize-handlermacro or similar. Currently the application has to set this up manually. - Framebuffer coordinate management — the framebuffer API needs to support partial region updates (the passepartout dirty flags map to specific areas: status bar rows 0-2, chat rows 3 to h-2, input row h-1).
- Non-blocking read-event — already supported via
:timeoutkeyword but should be documented as the main loop pattern.
Files to Modify
| File | Change |
|---|---|
passepartout.asd |
Add :cl-tty, remove :croatoan |
org/channel-tui-state.org |
Package uses, theme-color returns hex |
org/channel-tui-main.org |
Replace main loop, input handling |
org/channel-tui-view.org |
Replace all Croatoan window ops |
Verification
After each step, the TUI should:
- Compile without Croatoan dependency
- Start and show status bar, empty chat, input line
- Accept keyboard input and display typed text
- Connect to daemon and show messages
- Support all keybindings (arrows, Ctrl, Tab, PageUp/Down)
- Support resize via SIGWINCH
- Render markdown (bold, code, URLs, code blocks)
- Show gate traces with collapsible toggle
- All view and markdown tests pass (test-char-width, parse-markdown-spans, etc.)