- 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
235 lines
9.3 KiB
Org Mode
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.)
|