Files
passepartout/docs/cl-tty-migration-plan.org
Hermes 757541c83b fix: close defun on-key with missing paren, complete cl-tty TUI migration
- 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
2026-05-12 21:35:14 +00:00

235 lines
9.3 KiB
Org Mode

#+TITLE: cl-tty TUI Migration Plan
#+AUTHOR: Hermes
#+DATE: 2026-05-12
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-tty~ to ~passepartout/tui~ system dependencies in .asd
- Remove ~:croatoan~ dependency
- Add cl-tty to Quicklisp/local-projects or install path
**Step 2: Replace ~with-screen~ with cl-tty init (30 min)**
Replace:
#+BEGIN_SRC lisp
(with-screen (scr :input-blocking nil :input-echoing nil :cursor-visible nil)
...)
#+END_SRC
With:
#+BEGIN_SRC lisp
(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))))
#+END_SRC
**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~/~height~ on window)
**Step 4: Wire framebuffer diffing (view.org + main.org, 1 hour)**
Replace per-window ~clear~+~refresh~ with cl-tty's framebuffer:
1. Create framebuffer at terminal size
2. Each render function draws render commands into the framebuffer
3. Main loop calls ~flush-framebuffer~ which 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-event~ returns structured ~key-event~ structs with ~:key~ and ~: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~:
#+BEGIN_SRC lisp
:resize-pending nil
#+END_SRC
Add a SIGWINCH handler:
#+BEGIN_SRC lisp
(sb-sys:enable-interrupt sb-posix:sigwinch
(lambda () (setf (st :resize-pending) t)))
#+END_SRC
In the main loop, check before rendering:
#+BEGIN_SRC lisp
(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)))
#+END_SRC
**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:
#+BEGIN_SRC lisp
(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"))))))
#+END_SRC
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:
1. **SIGWINCH handling** — cl-tty should provide a ~with-resize-handler~
macro or similar. Currently the application has to set this up manually.
2. **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).
3. **Non-blocking read-event** — already supported via ~:timeout~ keyword
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:
1. Compile without Croatoan dependency
2. Start and show status bar, empty chat, input line
3. Accept keyboard input and display typed text
4. Connect to daemon and show messages
5. Support all keybindings (arrows, Ctrl, Tab, PageUp/Down)
6. Support resize via SIGWINCH
7. Render markdown (bold, code, URLs, code blocks)
8. Show gate traces with collapsible toggle
9. All view and markdown tests pass (test-char-width, parse-markdown-spans, etc.)