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
This commit is contained in:
133
docs/ROADMAP.org
133
docs/ROADMAP.org
@@ -2606,3 +2606,136 @@ World models, temporal reasoning, goal persistence across restarts.
|
|||||||
- World models: Predictive models of user behavior, project dynamics, system state.
|
- World models: Predictive models of user behavior, project dynamics, system state.
|
||||||
- Temporal reasoning: Scheduling, deadlines, elapsed duration awareness.
|
- Temporal reasoning: Scheduling, deadlines, elapsed duration awareness.
|
||||||
- Goal persistence: Goals survive restarts. Long-term projects in memory-objects.
|
- Goal persistence: Goals survive restarts. Long-term projects in memory-objects.
|
||||||
|
|
||||||
|
* v0.7.3: cl-tty TUI Migration
|
||||||
|
|
||||||
|
** Summary
|
||||||
|
|
||||||
|
Replace Croatoan (ncurses CFFI) with cl-tty (pure CL, no FFI) as the
|
||||||
|
terminal rendering backend for the TUI channels. Original rationale:
|
||||||
|
Croatoan is broken, cl-tty was purpose-built for this use case.
|
||||||
|
|
||||||
|
** Architecture decisions
|
||||||
|
|
||||||
|
1. Keep passepartout's state model (plist via ~st~/~(setf st)~) and
|
||||||
|
event dispatch (~on-key~, ~on-daemon-msg~) unchanged. Only the
|
||||||
|
output path changes.
|
||||||
|
2. Use cl-tty's framebuffer-backend for rendering: draw to framebuffer
|
||||||
|
cells, then diff+flush to the real backend. This gives minimal
|
||||||
|
terminal writes for free (only changed cells are sent).
|
||||||
|
3. Main loop: ~cl-tty.input:with-raw-terminal~ + ~cl-tty.backend:with-terminal~
|
||||||
|
replaces ~croatoan:with-screen~.
|
||||||
|
4. Input: ~cl-tty.input:read-event~ with ~:timeout 0~ replaces
|
||||||
|
~croatoan:get-char~ + ~code-key~/~key-name~ conversion.
|
||||||
|
5. Resize: cl-tty's SIGWINCH handler + ~:resize~ event replaces
|
||||||
|
Croatoan's KEY_RESIZE (410).
|
||||||
|
6. Markdown rendering: drop passepartout's hand-rolled ~render-styled~
|
||||||
|
(no longer called from view-chat). Wire cl-tty's built-in markdown
|
||||||
|
renderer as a follow-up.
|
||||||
|
|
||||||
|
** Remaining work (in order)
|
||||||
|
|
||||||
|
*** DONE Update .asd: swap :croatoan for :cl-tty
|
||||||
|
|
||||||
|
The ~:passepartout/tui~ system no longer depends on ~:croatoan~.
|
||||||
|
Depends on ~:cl-tty~ instead.
|
||||||
|
|
||||||
|
*** DONE Remove Croatoan from package (state.org)
|
||||||
|
|
||||||
|
~:use :cl :croatoan ...~ → ~:use :cl ...~. Export list unchanged.
|
||||||
|
~theme-color~ returns hex strings (cl-tty compatible) instead of
|
||||||
|
Croatoan color keywords.
|
||||||
|
|
||||||
|
*** DONE Rewrite main loop (main.org)
|
||||||
|
|
||||||
|
~tui-main~ now uses ~with-raw-terminal~ + ~with-terminal~ + framebuffer.
|
||||||
|
Key dispatch uses ~read-event~ returning structured events instead of
|
||||||
|
raw Croatoan codes / ~code-key~ conversion. Resize handled by cl-tty's
|
||||||
|
~:resize~ event type.
|
||||||
|
|
||||||
|
*** DONE Rewrite view functions (view.org)
|
||||||
|
|
||||||
|
~view-status~, ~view-chat~, ~view-input~, ~redraw~ all rewritten to
|
||||||
|
take a framebuffer-backend and use ~cl-tty.backend:draw-text~ instead
|
||||||
|
of Croatoan window operations (~add-string~, ~clear~, ~box~, ~refresh~).
|
||||||
|
|
||||||
|
*** TODO Fix render-styled (view.org)
|
||||||
|
|
||||||
|
~render-styled~ (Implementation section, v0.7.1 Markdown Rendering block)
|
||||||
|
still uses Croatoan's ~add-string~ and ~height~. This function is no
|
||||||
|
longer called from ~view-chat~ (replaced with plain ~draw-text~), but
|
||||||
|
it still exists in the source and causes a compile error because the
|
||||||
|
Croatoan package is no longer loaded.
|
||||||
|
|
||||||
|
Fix: Either (a) replace ~add-string~/~height~ with cl-tty equivalents
|
||||||
|
and keep the function for future styled markdown, or (b) remove it
|
||||||
|
entirely since it's dead code. Option (a) is preferred for
|
||||||
|
forward-compatibility.
|
||||||
|
|
||||||
|
Also remove ~height~ window dimension access (only used in
|
||||||
|
~render-styled~). Remove ~parse-markdown-spans~ if it's only called
|
||||||
|
from ~render-styled~ (it's not — it's used in the old view-chat
|
||||||
|
Croatoan code which has been replaced).
|
||||||
|
|
||||||
|
*** TODO Clean up render-styled's Croatoan references
|
||||||
|
|
||||||
|
~parse-markdown-spans~, ~syntax-highlight~, ~parse-markdown-blocks~,
|
||||||
|
and ~gate-trace-lines~ are all pure CL utility functions that don't
|
||||||
|
depend on Croatoan. Only ~render-styled~ itself uses Croatoan window
|
||||||
|
operations.
|
||||||
|
|
||||||
|
Concrete changes to ~render-styled~:
|
||||||
|
|
||||||
|
src="org/channel-tui-view.org" lang="diff"
|
||||||
|
-(defun render-styled (win segments y x w)
|
||||||
|
- "Render markdown segments to Croatoan window. Returns next y."
|
||||||
|
- (dolist (seg segments)
|
||||||
|
- (when (>= y (height win)) (return y))
|
||||||
|
- (let* ((text (or (car seg) ""))
|
||||||
|
- (attrs (cdr seg))
|
||||||
|
- (bold (getf attrs :bold))
|
||||||
|
- (code (getf attrs :code))
|
||||||
|
- (underline (getf attrs :underline))
|
||||||
|
- (url (getf attrs :url)))
|
||||||
|
- (add-string win text :y y :x x :n (max 1 (- w x))
|
||||||
|
- :bold bold :underline underline
|
||||||
|
- :bgcolor (when code (theme-color :dim))
|
||||||
|
- :fgcolor (cond (url (theme-color :highlight))
|
||||||
|
- (t (theme-color (or (getf attrs :role) :agent)))))
|
||||||
|
- (incf x (length text))))
|
||||||
|
- y)
|
||||||
|
+(defun render-styled (fb segments y x w)
|
||||||
|
+ "Render markdown segments to framebuffer. Returns next y."
|
||||||
|
+ (dolist (seg segments)
|
||||||
|
+ (let* ((text (or (car seg) ""))
|
||||||
|
+ (attrs (cdr seg))
|
||||||
|
+ (bold (getf attrs :bold))
|
||||||
|
+ (code (getf attrs :code))
|
||||||
|
+ (url (getf attrs :url)))
|
||||||
|
+ (cl-tty.backend:draw-text fb x y text
|
||||||
|
+ (cond (url (theme-color :highlight))
|
||||||
|
+ (t (theme-color (or (getf attrs :role) :agent))))
|
||||||
|
+ nil :bold bold)
|
||||||
|
+ (incf x (length text))))
|
||||||
|
+ y)
|
||||||
|
"""
|
||||||
|
|
||||||
|
*** TODO Tangled view.lisp: remove #+end_src / #+begin_src artifacts
|
||||||
|
|
||||||
|
The ~#+end_src~ and ~#+begin_src~ lines from the org are appearing
|
||||||
|
inside the tangled lisp file because the code blocks were split without
|
||||||
|
adjusting the org structure. Clean up any org artifacts in the generated
|
||||||
|
.lisp files.
|
||||||
|
|
||||||
|
*** TODO Verify compilation
|
||||||
|
|
||||||
|
1. ~cl-tty.input:with-raw-terminal~ resolves (stty-based, defined in
|
||||||
|
text-input.org → input.lisp, exported from cl-tty.input package)
|
||||||
|
2. ~cl-tty.backend:draw-text~ resolves on framebuffer-backend
|
||||||
|
3. ~cl-tty.rendering:flush-framebuffer~, ~make-framebuffer~ resolve
|
||||||
|
4. ~cl-tty.input:read-event~ returns structured events
|
||||||
|
5. ~cl-tty.input:*terminal-resized-p*~ exported
|
||||||
|
6. ~cl-tty.rendering:backend-clear~ dispatches on framebuffer-backend
|
||||||
|
|
||||||
|
*** TODO Push branch and let user test
|
||||||
|
|
||||||
|
|||||||
234
docs/cl-tty-migration-plan.org
Normal file
234
docs/cl-tty-migration-plan.org
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
#+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.)
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
when content
|
when content
|
||||||
do (let ((pos (or (search "https://" content) (search "http://" content))))
|
do (let ((pos (or (search "https://" content) (search "http://" content))))
|
||||||
(when pos
|
(when pos
|
||||||
(let ((end (or (position-if (lambda (c) (find c '(#\Space #\Newline #\Tab #\))))
|
(let ((end (or (position-if (lambda (c) (find c (list #\Space #\Newline #\Tab (code-char 41))))
|
||||||
content :start pos)
|
content :start pos)
|
||||||
(length content))))
|
(length content))))
|
||||||
(setf url (subseq content pos end))
|
(setf url (subseq content pos end))
|
||||||
@@ -781,70 +781,80 @@
|
|||||||
(init-state)
|
(init-state)
|
||||||
(load-history)
|
(load-history)
|
||||||
(theme-load)
|
(theme-load)
|
||||||
(with-screen (scr :input-blocking nil :input-echoing nil :cursor-visible nil)
|
(let* ((swank-port (or (ignore-errors
|
||||||
(let* ((h (or (height scr) 24))
|
(parse-integer (uiop:getenv "TUI_SWANK_PORT")))
|
||||||
(w (or (width scr) 80))
|
4006)))
|
||||||
(sw (make-instance 'window :height 3 :width (- w 2) :y 0 :x 1))
|
(setf (st :dirty) (list t t t))
|
||||||
(ch (- h 5))
|
(connect-daemon)
|
||||||
(cw (make-instance 'window :height ch :width (- w 2) :y 3 :x 1))
|
(when (> swank-port 0)
|
||||||
(iw (make-instance 'window :height 1 :width (- w 2) :y (- h 1) :x 1))
|
(handler-case
|
||||||
(swank-port (or (ignore-errors
|
(progn
|
||||||
(parse-integer (uiop:getenv "TUI_SWANK_PORT")))
|
(ql:quickload :swank :silent t)
|
||||||
4006)))
|
(funcall (find-symbol "CREATE-SERVER" "SWANK")
|
||||||
(setf (function-keys-enabled-p iw) t
|
:port swank-port :dont-close t)
|
||||||
(input-blocking iw) nil
|
(add-msg :system
|
||||||
(st :dirty) (list t t t)
|
(format nil "* Swank ~d M-x slime-connect *" swank-port)))
|
||||||
;; Store windows in state for SIGWINCH handler
|
(error ()
|
||||||
(st :scr) scr (st :sw) sw (st :cw) cw (st :iw) iw)
|
(add-msg :system "* Swank unavailable *"))))
|
||||||
(connect-daemon)
|
(cl-tty.input:with-raw-terminal
|
||||||
(when (> swank-port 0)
|
(cl-tty.backend:with-terminal (be w h)
|
||||||
(handler-case
|
(let ((prev-fb (cl-tty.rendering:make-framebuffer w h))
|
||||||
(progn
|
(curr-fb (cl-tty.rendering:make-framebuffer w h)))
|
||||||
(ql:quickload :swank :silent t)
|
;; Initial render
|
||||||
(funcall (find-symbol "CREATE-SERVER" "SWANK")
|
(redraw be curr-fb w h)
|
||||||
:port swank-port :dont-close t)
|
(cl-tty.rendering:flush-framebuffer prev-fb curr-fb be)
|
||||||
(add-msg :system
|
(rotatef prev-fb curr-fb)
|
||||||
(format nil "* Swank ~d M-x slime-connect *" swank-port)))
|
(loop while (st :running) do
|
||||||
(error ()
|
(dolist (ev (drain-queue))
|
||||||
(add-msg :system "* Swank unavailable *"))))
|
(cond
|
||||||
;; Initial render before the main loop — otherwise the screen stays
|
((eq (getf ev :type) :daemon)
|
||||||
;; blank until the first keystroke (get-char blocks).
|
(on-daemon-msg (getf ev :payload)))
|
||||||
(redraw sw cw ch iw)
|
((eq (getf ev :type) :disconnected)
|
||||||
(refresh scr)
|
(setf (st :connected) nil
|
||||||
(loop while (st :running) do
|
(st :busy) nil)
|
||||||
(dolist (ev (drain-queue))
|
(add-msg :system "* Connection lost — type /reconnect to retry *"))))
|
||||||
(cond
|
(multiple-value-bind (type data)
|
||||||
((eq (getf ev :type) :daemon)
|
(cl-tty.input:read-event be :timeout 0)
|
||||||
(on-daemon-msg (getf ev :payload)))
|
(cond
|
||||||
((eq (getf ev :type) :disconnected)
|
((eq type :resize)
|
||||||
(setf (st :connected) nil
|
(multiple-value-setq (w h) (cl-tty.backend:backend-size be))
|
||||||
(st :busy) nil)
|
(setf prev-fb (cl-tty.rendering:make-framebuffer w h)
|
||||||
(add-msg :system "* Connection lost — type /reconnect to retry *"))))
|
curr-fb (cl-tty.rendering:make-framebuffer w h))
|
||||||
(let ((ch (get-char iw)))
|
(setf (st :dirty) (list t t t)))
|
||||||
(cond
|
(data
|
||||||
((or (not ch) (equal ch -1)) nil)
|
(let ((ch (typecase data
|
||||||
;; KEY_RESIZE — terminal was resized (SIGWINCH from ncurses)
|
(cl-tty.input:key-event
|
||||||
((eql ch 410)
|
(cl-tty.input:key-event-key data))
|
||||||
(let* ((new-h (or (height scr) 24))
|
(t data))))
|
||||||
(new-w (or (width scr) 80))
|
(cond
|
||||||
(new-ch (- new-h 5)))
|
((eql ch :escape)
|
||||||
(setq sw (make-instance 'window :height 3 :width (- new-w 2) :y 0 :x 1)
|
(when (st :streaming-text)
|
||||||
ch new-ch
|
(send-daemon (list :type :event :payload '(:action :cancel-stream)))
|
||||||
cw (make-instance 'window :height new-ch :width (- new-w 2) :y 3 :x 1)
|
(when (> (length (st :messages)) 0)
|
||||||
iw (make-instance 'window :height 1 :width (- new-w 2) :y (- new-h 1) :x 1)
|
(let ((idx (1- (length (st :messages)))))
|
||||||
w new-w
|
(setf (getf (aref (st :messages) idx) :content)
|
||||||
h new-h)
|
(concatenate 'string
|
||||||
(setf (function-keys-enabled-p iw) t
|
(getf (aref (st :messages) idx) :content)
|
||||||
(input-blocking iw) nil
|
" [interrupted]"))
|
||||||
(st :dirty) (list t t t)
|
(setf (getf (aref (st :messages) idx) :streaming) nil)
|
||||||
(st :sw) sw (st :cw) cw (st :iw) iw)
|
(setf (getf (aref (st :messages) idx) :time) (now))))
|
||||||
(redraw sw cw ch iw)
|
(setf (st :streaming-text) nil)
|
||||||
(refresh scr)))
|
(setf (st :busy) nil)
|
||||||
(t (on-key ch))))
|
(setf (st :dirty) (list t t nil)))
|
||||||
(redraw sw cw ch iw)
|
(when (st :search-mode)
|
||||||
(refresh scr)
|
(setf (st :search-mode) nil
|
||||||
(sleep 0.03))
|
(st :search-matches) nil
|
||||||
(disconnect-daemon))))
|
(st :search-query) "")
|
||||||
|
(setf (st :dirty) (list nil t nil))
|
||||||
|
(add-msg :system "Search exited")))
|
||||||
|
(t (on-key ch)))))))
|
||||||
|
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
||||||
|
(cl-tty.backend:backend-clear curr-fb)
|
||||||
|
(redraw be curr-fb w h)
|
||||||
|
(cl-tty.rendering:flush-framebuffer prev-fb curr-fb be)
|
||||||
|
(rotatef prev-fb curr-fb))
|
||||||
|
(sleep 0.1))))
|
||||||
|
(disconnect-daemon))))
|
||||||
|
|
||||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
(ql:quickload :fiveam :silent t))
|
(ql:quickload :fiveam :silent t))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
(defpackage :passepartout.channel-tui
|
(defpackage :passepartout.channel-tui
|
||||||
(:use :cl :croatoan :passepartout :usocket :bordeaux-threads)
|
(:use :cl :passepartout :usocket :bordeaux-threads)
|
||||||
(:export :tui-main :st :add-msg :now :input-string
|
(:export :tui-main :st :add-msg :now :input-string
|
||||||
:queue-event :drain-queue :init-state
|
:queue-event :drain-queue :init-state
|
||||||
:view-status :view-chat :view-input :redraw
|
:view-status :view-chat :view-input :redraw
|
||||||
@@ -101,8 +101,15 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
|
|||||||
key)))
|
key)))
|
||||||
|
|
||||||
(defun theme-color (role)
|
(defun theme-color (role)
|
||||||
"Returns the Croatoan color for a semantic role."
|
"Returns a hex color string for a semantic role, suitable for cl-tty."
|
||||||
(or (getf *tui-theme* role) :white))
|
(let ((val (or (getf *tui-theme* role) :white)))
|
||||||
|
(cond
|
||||||
|
((stringp val) val)
|
||||||
|
(t (case val
|
||||||
|
(:green "#00FF00") (:red "#FF0000") (:cyan "#00FFFF")
|
||||||
|
(:yellow "#FFFF00") (:magenta "#FF00FF") (:blue "#0000FF")
|
||||||
|
(:white "#FFFFFF") (:black "#000000")
|
||||||
|
(t "#FFFFFF"))))))
|
||||||
|
|
||||||
(defun st (key) (getf *state* key))
|
(defun st (key) (getf *state* key))
|
||||||
(defun (setf st) (val key) (setf (getf *state* key) val))
|
(defun (setf st) (val key) (setf (getf *state* key) val))
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
(in-package :passepartout.channel-tui)
|
(in-package :passepartout.channel-tui)
|
||||||
|
|
||||||
(defun view-status (win)
|
(defun view-status (fb w)
|
||||||
(clear win)
|
(let ((line1 (format nil
|
||||||
(box win 0 0)
|
" Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a"
|
||||||
(add-string win
|
|
||||||
(format nil " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a"
|
|
||||||
(if (st :connected) "● Connected" "○ Disconnected")
|
(if (st :connected) "● Connected" "○ Disconnected")
|
||||||
(string-upcase (string (st :mode)))
|
(string-upcase (string (st :mode)))
|
||||||
(length (st :messages))
|
(length (st :messages))
|
||||||
(if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0")
|
(if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0")
|
||||||
(or (st :rule-count) 0)
|
(or (st :rule-count) 0)
|
||||||
(if (st :streaming-text) " [streaming]"
|
(if (st :streaming-text) " [streaming]"
|
||||||
(if (st :busy) " …thinking" "")))
|
(if (st :busy) " …thinking" "")))))
|
||||||
:y 1 :x 1 :fgcolor (theme-color (if (st :connected) :connected :disconnected)))
|
(cl-tty.backend:draw-text fb 1 1 line1
|
||||||
;; Second line: Focus map (left) + timestamp (right-aligned, v0.7.0)
|
(theme-color (if (st :connected) :connected :disconnected))
|
||||||
(let ((focus-info (or (st :foveal-id) "")))
|
nil)
|
||||||
(when (and focus-info (> (length focus-info) 0))
|
;; Second line: Focus map (left) + timestamp (right-aligned, v0.7.0)
|
||||||
(add-string win (format nil " [Focus: ~a]" focus-info)
|
(let ((focus-info (or (st :foveal-id) "")))
|
||||||
:y 2 :x 1 :fgcolor (theme-color :timestamp))))
|
(when (and focus-info (> (length focus-info) 0))
|
||||||
(add-string win (format nil " ~a" (now))
|
(cl-tty.backend:draw-text fb 1 2 (format nil " [Focus: ~a]" focus-info)
|
||||||
:y 2 :x (max 1 (- (width win) 12))
|
(theme-color :timestamp) nil)))
|
||||||
:fgcolor (theme-color :timestamp))
|
(cl-tty.backend:draw-text fb (max 1 (- w 12)) 2 (format nil " ~a" (now))
|
||||||
(refresh win))
|
(theme-color :timestamp) nil)))
|
||||||
|
|
||||||
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
|
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
|
||||||
(defun search-highlight (content query)
|
(defun search-highlight (content query)
|
||||||
@@ -40,11 +38,8 @@
|
|||||||
(setf result (concatenate 'string result (subseq content pos)))
|
(setf result (concatenate 'string result (subseq content pos)))
|
||||||
(if (string= result "") content result))))
|
(if (string= result "") content result))))
|
||||||
|
|
||||||
(defun view-chat (win h)
|
(defun view-chat (fb w h)
|
||||||
(clear win)
|
(let* ((msgs (st :messages))
|
||||||
(box win 0 0)
|
|
||||||
(let* ((w (or (width win) 78))
|
|
||||||
(msgs (st :messages))
|
|
||||||
(total (length msgs))
|
(total (length msgs))
|
||||||
(max-lines (- h 2))
|
(max-lines (- h 2))
|
||||||
(is-search (st :search-mode))
|
(is-search (st :search-mode))
|
||||||
@@ -56,7 +51,7 @@
|
|||||||
(query (st :search-query))
|
(query (st :search-query))
|
||||||
(header (format nil "Search: ~d matches for '~a' (~d/~d) — Esc to exit"
|
(header (format nil "Search: ~d matches for '~a' (~d/~d) — Esc to exit"
|
||||||
(length matches) query (1+ idx) (length matches))))
|
(length matches) query (1+ idx) (length matches))))
|
||||||
(add-string win header :y y :x 1 :n (1- w) :fgcolor (theme-color :highlight))
|
(cl-tty.backend:draw-text fb 1 y header (theme-color :highlight) nil)
|
||||||
(incf y)
|
(incf y)
|
||||||
(decf max-lines)))
|
(decf max-lines)))
|
||||||
;; Count visible messages from end, accounting for word wrap
|
;; Count visible messages from end, accounting for word wrap
|
||||||
@@ -65,14 +60,14 @@
|
|||||||
(loop for i from (1- total) downto 0
|
(loop for i from (1- total) downto 0
|
||||||
while (> lines-remaining 0)
|
while (> lines-remaining 0)
|
||||||
do (let* ((msg (aref msgs i))
|
do (let* ((msg (aref msgs i))
|
||||||
(role (getf msg :role))
|
(role (getf msg :role))
|
||||||
(content (getf msg :content))
|
(content (getf msg :content))
|
||||||
(time (or (getf msg :time) ""))
|
(time (or (getf msg :time) ""))
|
||||||
(prefix (case role (:user "⬆") (:agent "⬇") (t " ")))
|
(prefix (case role (:user "⬆") (:agent "⬇") (t " ")))
|
||||||
(content-show (if is-search
|
(content-show (if is-search
|
||||||
(search-highlight content (st :search-query))
|
(search-highlight content (st :search-query))
|
||||||
content))
|
content))
|
||||||
(line-text (format nil "~a [~a] ~a" prefix time content-show))
|
(line-text (format nil "~a [~a] ~a" prefix time content-show))
|
||||||
(wrapped (word-wrap line-text (- w 2)))
|
(wrapped (word-wrap line-text (- w 2)))
|
||||||
(nlines (length wrapped)))
|
(nlines (length wrapped)))
|
||||||
(if (<= nlines lines-remaining)
|
(if (<= nlines lines-remaining)
|
||||||
@@ -103,38 +98,30 @@
|
|||||||
(theme-color :hitl))))
|
(theme-color :hitl))))
|
||||||
(dolist (line wrapped)
|
(dolist (line wrapped)
|
||||||
(when (< y (1- h))
|
(when (< y (1- h))
|
||||||
(if (eq role :agent)
|
(cl-tty.backend:draw-text fb 1 y line color nil)
|
||||||
(let ((segments (parse-markdown-spans line)))
|
(incf y)))
|
||||||
(setf y (render-styled win segments y 1 w)))
|
|
||||||
(progn
|
|
||||||
(add-string win line :y y :x 1 :n (1- w) :fgcolor color)
|
|
||||||
(incf y)))))
|
|
||||||
;; v0.7.2: gate trace below agent messages
|
;; v0.7.2: gate trace below agent messages
|
||||||
(let ((gate-trace (getf msg :gate-trace)))
|
(let ((gate-trace (getf msg :gate-trace)))
|
||||||
(when (and gate-trace (not (member i (st :collapsed-gates))))
|
(when (and gate-trace (not (member i (st :collapsed-gates))))
|
||||||
(dolist (entry (passepartout::gate-trace-lines gate-trace))
|
(dolist (entry (passepartout::gate-trace-lines gate-trace))
|
||||||
(when (< y (1- h))
|
(when (< y (1- h))
|
||||||
(add-string win (car entry) :y y :x 3 :n (- w 4) :fgcolor (or (getf (cdr entry) :fgcolor) :dim))
|
(cl-tty.backend:draw-text fb 3 y (car entry)
|
||||||
(incf y))))))))))
|
(or (getf (cdr entry) :fgcolor) :dim) nil)
|
||||||
(refresh win))
|
(incf y)))))))))))
|
||||||
|
|
||||||
(defun view-input (win)
|
(defun view-input (fb w)
|
||||||
(let* ((text (input-string))
|
(let* ((text (input-string))
|
||||||
(w (or (width win) 78))
|
|
||||||
(pos (or (st :cursor-pos) 0))
|
(pos (or (st :cursor-pos) 0))
|
||||||
(display-start (max 0 (- pos (1- w))))
|
(display-start (max 0 (- pos (1- w))))
|
||||||
(visible (subseq text display-start (min (length text) (+ display-start w)))))
|
(visible (subseq text display-start (min (length text) (+ display-start w)))))
|
||||||
(clear win)
|
(cl-tty.backend:draw-text fb 0 0 (format nil "~a " visible) (theme-color :input) nil)))
|
||||||
(add-string win (format nil "~a " visible) :y 0 :x 0 :n (1- w) :fgcolor (theme-color :input))
|
|
||||||
(setf (cursor-position win) (list 0 (min (- pos display-start) (1- w)))))
|
|
||||||
(refresh win))
|
|
||||||
|
|
||||||
(defun redraw (sw cw ch iw)
|
(defun redraw (fb w h)
|
||||||
(destructuring-bind (sd cd id) (st :dirty)
|
(destructuring-bind (sd cd id) (st :dirty)
|
||||||
(when sd (view-status sw))
|
(when sd (view-status fb w))
|
||||||
(when cd (view-chat cw ch))
|
(when cd (view-chat fb w (- h 5)))
|
||||||
(when id (view-input iw))
|
(when id (view-input fb w))
|
||||||
(setf (st :dirty) (list nil nil nil))))
|
(setf (st :dirty) (list nil nil nil))))
|
||||||
|
|
||||||
(in-package :passepartout)
|
(in-package :passepartout)
|
||||||
|
|
||||||
@@ -200,21 +187,20 @@ ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8."
|
|||||||
(t (push (cons (subseq text pos) nil) results) (return))))))))
|
(t (push (cons (subseq text pos) nil) results) (return))))))))
|
||||||
(nreverse results)))
|
(nreverse results)))
|
||||||
|
|
||||||
(defun render-styled (win segments y x w)
|
(defun render-styled (fb segments y x w)
|
||||||
"Render markdown segments to Croatoan window. Returns next y."
|
"Render markdown segments to cl-tty backend. Returns next y."
|
||||||
(dolist (seg segments)
|
(dolist (seg segments)
|
||||||
(when (>= y (height win)) (return y))
|
|
||||||
(let* ((text (or (car seg) ""))
|
(let* ((text (or (car seg) ""))
|
||||||
(attrs (cdr seg))
|
(attrs (cdr seg))
|
||||||
(bold (getf attrs :bold))
|
(bold (getf attrs :bold))
|
||||||
(code (getf attrs :code))
|
(code (getf attrs :code))
|
||||||
(underline (getf attrs :underline))
|
|
||||||
(url (getf attrs :url)))
|
(url (getf attrs :url)))
|
||||||
(add-string win text :y y :x x :n (max 1 (- w x))
|
(declare (ignore code))
|
||||||
:bold bold :underline underline
|
(cl-tty.backend:draw-text fb x y text
|
||||||
:bgcolor (when code (theme-color :dim))
|
(cond (url (theme-color :highlight))
|
||||||
:fgcolor (cond (url (theme-color :highlight))
|
(t (theme-color (or (getf attrs :role) :agent))))
|
||||||
(t (theme-color (or (getf attrs :role) :agent)))))
|
nil
|
||||||
|
:bold bold)
|
||||||
(incf x (length text))))
|
(incf x (length text))))
|
||||||
y)
|
y)
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Event handlers + daemon I/O + main loop.
|
|||||||
render/input event loop at ~30fps.
|
render/input event loop at ~30fps.
|
||||||
|
|
||||||
** Event Handlers
|
** Event Handlers
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp
|
||||||
(in-package :passepartout.channel-tui)
|
(in-package :passepartout.channel-tui)
|
||||||
|
|
||||||
(defun on-key (&rest args)
|
(defun on-key (&rest args)
|
||||||
@@ -114,7 +114,7 @@ Event handlers + daemon I/O + main loop.
|
|||||||
when content
|
when content
|
||||||
do (let ((pos (or (search "https://" content) (search "http://" content))))
|
do (let ((pos (or (search "https://" content) (search "http://" content))))
|
||||||
(when pos
|
(when pos
|
||||||
(let ((end (or (position-if (lambda (c) (find c '(#\Space #\Newline #\Tab #\))))
|
(let ((end (or (position-if (lambda (c) (find c (list #\Space #\Newline #\Tab (code-char 41))))
|
||||||
content :start pos)
|
content :start pos)
|
||||||
(length content))))
|
(length content))))
|
||||||
(setf url (subseq content pos end))
|
(setf url (subseq content pos end))
|
||||||
@@ -729,10 +729,10 @@ Event handlers + daemon I/O + main loop.
|
|||||||
((eq action :handshake)
|
((eq action :handshake)
|
||||||
(add-msg :system (format nil "Connected v~a" (getf payload :version))))
|
(add-msg :system (format nil "Connected v~a" (getf payload :version))))
|
||||||
(t (add-msg :agent (format nil "~a" msg))))))
|
(t (add-msg :agent (format nil "~a" msg))))))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
** Daemon Communication
|
** Daemon Communication
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp
|
||||||
(defun send-daemon (msg)
|
(defun send-daemon (msg)
|
||||||
(let ((s (st :stream)))
|
(let ((s (st :stream)))
|
||||||
(when (and s (open-stream-p s))
|
(when (and s (open-stream-p s))
|
||||||
@@ -783,10 +783,10 @@ Event handlers + daemon I/O + main loop.
|
|||||||
while line
|
while line
|
||||||
do (push line (st :input-history))))
|
do (push line (st :input-history))))
|
||||||
(setf (st :input-history) (nreverse (st :input-history))))))
|
(setf (st :input-history) (nreverse (st :input-history))))))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
** Connection
|
** Connection
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp
|
||||||
(defun connect-daemon (&optional (host "127.0.0.1") (port 9105))
|
(defun connect-daemon (&optional (host "127.0.0.1") (port 9105))
|
||||||
(add-msg :system "* Connecting to daemon... *")
|
(add-msg :system "* Connecting to daemon... *")
|
||||||
(loop for attempt from 1 to 3
|
(loop for attempt from 1 to 3
|
||||||
@@ -816,83 +816,92 @@ Event handlers + daemon I/O + main loop.
|
|||||||
(ignore-errors (close (st :stream)))
|
(ignore-errors (close (st :stream)))
|
||||||
(setf (st :stream) nil (st :connected) nil)
|
(setf (st :stream) nil (st :connected) nil)
|
||||||
(add-msg :system "* Disconnected *")))
|
(add-msg :system "* Disconnected *")))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
** Main Loop
|
** Main Loop
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp
|
||||||
(defun tui-main ()
|
(defun tui-main ()
|
||||||
(init-state)
|
(init-state)
|
||||||
(load-history)
|
(load-history)
|
||||||
(theme-load)
|
(theme-load)
|
||||||
(with-screen (scr :input-blocking nil :input-echoing nil :cursor-visible nil)
|
(let* ((swank-port (or (ignore-errors
|
||||||
(let* ((h (or (height scr) 24))
|
(parse-integer (uiop:getenv "TUI_SWANK_PORT")))
|
||||||
(w (or (width scr) 80))
|
4006)))
|
||||||
(sw (make-instance 'window :height 3 :width (- w 2) :y 0 :x 1))
|
(setf (st :dirty) (list t t t))
|
||||||
(ch (- h 5))
|
(connect-daemon)
|
||||||
(cw (make-instance 'window :height ch :width (- w 2) :y 3 :x 1))
|
(when (> swank-port 0)
|
||||||
(iw (make-instance 'window :height 1 :width (- w 2) :y (- h 1) :x 1))
|
(handler-case
|
||||||
(swank-port (or (ignore-errors
|
(progn
|
||||||
(parse-integer (uiop:getenv "TUI_SWANK_PORT")))
|
(ql:quickload :swank :silent t)
|
||||||
4006)))
|
(funcall (find-symbol "CREATE-SERVER" "SWANK")
|
||||||
(setf (function-keys-enabled-p iw) t
|
:port swank-port :dont-close t)
|
||||||
(input-blocking iw) nil
|
(add-msg :system
|
||||||
(st :dirty) (list t t t)
|
(format nil "* Swank ~d M-x slime-connect *" swank-port)))
|
||||||
;; Store windows in state for SIGWINCH handler
|
(error ()
|
||||||
(st :scr) scr (st :sw) sw (st :cw) cw (st :iw) iw)
|
(add-msg :system "* Swank unavailable *"))))
|
||||||
(connect-daemon)
|
(cl-tty.input:with-raw-terminal
|
||||||
(when (> swank-port 0)
|
(cl-tty.backend:with-terminal (be w h)
|
||||||
(handler-case
|
(let ((prev-fb (cl-tty.rendering:make-framebuffer w h))
|
||||||
(progn
|
(curr-fb (cl-tty.rendering:make-framebuffer w h)))
|
||||||
(ql:quickload :swank :silent t)
|
;; Initial render
|
||||||
(funcall (find-symbol "CREATE-SERVER" "SWANK")
|
(redraw be curr-fb w h)
|
||||||
:port swank-port :dont-close t)
|
(cl-tty.rendering:flush-framebuffer prev-fb curr-fb be)
|
||||||
(add-msg :system
|
(rotatef prev-fb curr-fb)
|
||||||
(format nil "* Swank ~d M-x slime-connect *" swank-port)))
|
(loop while (st :running) do
|
||||||
(error ()
|
(dolist (ev (drain-queue))
|
||||||
(add-msg :system "* Swank unavailable *"))))
|
(cond
|
||||||
;; Initial render before the main loop — otherwise the screen stays
|
((eq (getf ev :type) :daemon)
|
||||||
;; blank until the first keystroke (get-char blocks).
|
(on-daemon-msg (getf ev :payload)))
|
||||||
(redraw sw cw ch iw)
|
((eq (getf ev :type) :disconnected)
|
||||||
(refresh scr)
|
(setf (st :connected) nil
|
||||||
(loop while (st :running) do
|
(st :busy) nil)
|
||||||
(dolist (ev (drain-queue))
|
(add-msg :system "* Connection lost — type /reconnect to retry *"))))
|
||||||
(cond
|
(multiple-value-bind (type data)
|
||||||
((eq (getf ev :type) :daemon)
|
(cl-tty.input:read-event be :timeout 0)
|
||||||
(on-daemon-msg (getf ev :payload)))
|
(cond
|
||||||
((eq (getf ev :type) :disconnected)
|
((eq type :resize)
|
||||||
(setf (st :connected) nil
|
(multiple-value-setq (w h) (cl-tty.backend:backend-size be))
|
||||||
(st :busy) nil)
|
(setf prev-fb (cl-tty.rendering:make-framebuffer w h)
|
||||||
(add-msg :system "* Connection lost — type /reconnect to retry *"))))
|
curr-fb (cl-tty.rendering:make-framebuffer w h))
|
||||||
(let ((ch (get-char iw)))
|
(setf (st :dirty) (list t t t)))
|
||||||
(cond
|
(data
|
||||||
((or (not ch) (equal ch -1)) nil)
|
(let ((ch (typecase data
|
||||||
;; KEY_RESIZE — terminal was resized (SIGWINCH from ncurses)
|
(cl-tty.input:key-event
|
||||||
((eql ch 410)
|
(cl-tty.input:key-event-key data))
|
||||||
(let* ((new-h (or (height scr) 24))
|
(t data))))
|
||||||
(new-w (or (width scr) 80))
|
(cond
|
||||||
(new-ch (- new-h 5)))
|
((eql ch :escape)
|
||||||
(setq sw (make-instance 'window :height 3 :width (- new-w 2) :y 0 :x 1)
|
(when (st :streaming-text)
|
||||||
ch new-ch
|
(send-daemon (list :type :event :payload '(:action :cancel-stream)))
|
||||||
cw (make-instance 'window :height new-ch :width (- new-w 2) :y 3 :x 1)
|
(when (> (length (st :messages)) 0)
|
||||||
iw (make-instance 'window :height 1 :width (- new-w 2) :y (- new-h 1) :x 1)
|
(let ((idx (1- (length (st :messages)))))
|
||||||
w new-w
|
(setf (getf (aref (st :messages) idx) :content)
|
||||||
h new-h)
|
(concatenate 'string
|
||||||
(setf (function-keys-enabled-p iw) t
|
(getf (aref (st :messages) idx) :content)
|
||||||
(input-blocking iw) nil
|
" [interrupted]"))
|
||||||
(st :dirty) (list t t t)
|
(setf (getf (aref (st :messages) idx) :streaming) nil)
|
||||||
(st :sw) sw (st :cw) cw (st :iw) iw)
|
(setf (getf (aref (st :messages) idx) :time) (now))))
|
||||||
(redraw sw cw ch iw)
|
(setf (st :streaming-text) nil)
|
||||||
(refresh scr)))
|
(setf (st :busy) nil)
|
||||||
(t (on-key ch))))
|
(setf (st :dirty) (list t t nil)))
|
||||||
(redraw sw cw ch iw)
|
(when (st :search-mode)
|
||||||
(refresh scr)
|
(setf (st :search-mode) nil
|
||||||
(sleep 0.03))
|
(st :search-matches) nil
|
||||||
(disconnect-daemon))))
|
(st :search-query) "")
|
||||||
|
(setf (st :dirty) (list nil t nil))
|
||||||
#+end_src
|
(add-msg :system "Search exited")))
|
||||||
|
(t (on-key ch)))))))
|
||||||
|
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
||||||
|
(cl-tty.backend:backend-clear curr-fb)
|
||||||
|
(redraw be curr-fb w h)
|
||||||
|
(cl-tty.rendering:flush-framebuffer prev-fb curr-fb be)
|
||||||
|
(rotatef prev-fb curr-fb))
|
||||||
|
(sleep 0.1))))
|
||||||
|
(disconnect-daemon))))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
* Test Suite
|
* Test Suite
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp
|
||||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
(ql:quickload :fiveam :silent t))
|
(ql:quickload :fiveam :silent t))
|
||||||
|
|
||||||
@@ -1367,4 +1376,4 @@ Event handlers + daemon I/O + main loop.
|
|||||||
(setf (st :scroll-offset) 3)
|
(setf (st :scroll-offset) 3)
|
||||||
(on-key :npage)
|
(on-key :npage)
|
||||||
(fiveam:is (= 0 (st :scroll-offset))))
|
(fiveam:is (= 0 (st :scroll-offset))))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ All state mutation flows through event handlers in the controller.
|
|||||||
reader loop. (drain-queue) returns and clears the queue.
|
reader loop. (drain-queue) returns and clears the queue.
|
||||||
|
|
||||||
** Package + State
|
** Package + State
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-state.lisp
|
||||||
(defpackage :passepartout.channel-tui
|
(defpackage :passepartout.channel-tui
|
||||||
(:use :cl :croatoan :passepartout :usocket :bordeaux-threads)
|
(:use :cl :passepartout :usocket :bordeaux-threads)
|
||||||
(:export :tui-main :st :add-msg :now :input-string
|
(:export :tui-main :st :add-msg :now :input-string
|
||||||
:queue-event :drain-queue :init-state
|
:queue-event :drain-queue :init-state
|
||||||
:view-status :view-chat :view-input :redraw
|
:view-status :view-chat :view-input :redraw
|
||||||
@@ -121,8 +121,15 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
|
|||||||
key)))
|
key)))
|
||||||
|
|
||||||
(defun theme-color (role)
|
(defun theme-color (role)
|
||||||
"Returns the Croatoan color for a semantic role."
|
"Returns a hex color string for a semantic role, suitable for cl-tty."
|
||||||
(or (getf *tui-theme* role) :white))
|
(let ((val (or (getf *tui-theme* role) :white)))
|
||||||
|
(cond
|
||||||
|
((stringp val) val)
|
||||||
|
(t (case val
|
||||||
|
(:green "#00FF00") (:red "#FF0000") (:cyan "#00FFFF")
|
||||||
|
(:yellow "#FFFF00") (:magenta "#FF00FF") (:blue "#0000FF")
|
||||||
|
(:white "#FFFFFF") (:black "#000000")
|
||||||
|
(t "#FFFFFF"))))))
|
||||||
|
|
||||||
(defun st (key) (getf *state* key))
|
(defun st (key) (getf *state* key))
|
||||||
(defun (setf st) (val key) (setf (getf *state* key) val))
|
(defun (setf st) (val key) (setf (getf *state* key) val))
|
||||||
@@ -140,10 +147,10 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
|
|||||||
:search-mode nil :search-query "" ; v0.7.2
|
:search-mode nil :search-query "" ; v0.7.2
|
||||||
:search-matches nil :search-match-idx 0
|
:search-matches nil :search-match-idx 0
|
||||||
:dirty (list nil nil nil))))
|
:dirty (list nil nil nil))))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
** Helpers
|
** Helpers
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-state.lisp
|
||||||
(defun now ()
|
(defun now ()
|
||||||
(multiple-value-bind (s m h) (get-decoded-time)
|
(multiple-value-bind (s m h) (get-decoded-time)
|
||||||
(declare (ignore s))
|
(declare (ignore s))
|
||||||
@@ -177,10 +184,10 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
|
|||||||
(unless (st :scroll-at-bottom)
|
(unless (st :scroll-at-bottom)
|
||||||
(setf (st :scroll-notify) t))
|
(setf (st :scroll-notify) t))
|
||||||
(setf (st :dirty) (list t t nil)))
|
(setf (st :dirty) (list t t nil)))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
** Event Queue
|
** Event Queue
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-state.lisp
|
||||||
(defun queue-event (ev)
|
(defun queue-event (ev)
|
||||||
(bt:with-lock-held (*event-lock*) (push ev *event-queue*)))
|
(bt:with-lock-held (*event-lock*) (push ev *event-queue*)))
|
||||||
|
|
||||||
@@ -188,4 +195,4 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
|
|||||||
(bt:with-lock-held (*event-lock*)
|
(bt:with-lock-held (*event-lock*)
|
||||||
(let ((evs (nreverse *event-queue*)))
|
(let ((evs (nreverse *event-queue*)))
|
||||||
(setf *event-queue* nil) evs)))
|
(setf *event-queue* nil) evs)))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
* View
|
* View
|
||||||
|
|
||||||
Pure render functions. Each takes a Croatoan window and current state.
|
|Pure render functions. Each takes the cl-tty backend and current state.
|
||||||
State is read via ~(st :key)~ — no mutation here.
|
|State is read via ~(st :key)~ — no mutation here.
|
||||||
|
|
||||||
** Contract
|
** Contract
|
||||||
|
|
||||||
@@ -42,31 +42,29 @@ architecture:
|
|||||||
All three enrichments cost 0 LLM tokens — they are daemon-state queries
|
All three enrichments cost 0 LLM tokens — they are daemon-state queries
|
||||||
that the TUI actuator attaches to the response plist before transmission.
|
that the TUI actuator attaches to the response plist before transmission.
|
||||||
|
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-view.lisp
|
||||||
(in-package :passepartout.channel-tui)
|
(in-package :passepartout.channel-tui)
|
||||||
|
|
||||||
(defun view-status (win)
|
(defun view-status (fb w)
|
||||||
(clear win)
|
(let ((line1 (format nil
|
||||||
(box win 0 0)
|
" Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a"
|
||||||
(add-string win
|
|
||||||
(format nil " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a"
|
|
||||||
(if (st :connected) "● Connected" "○ Disconnected")
|
(if (st :connected) "● Connected" "○ Disconnected")
|
||||||
(string-upcase (string (st :mode)))
|
(string-upcase (string (st :mode)))
|
||||||
(length (st :messages))
|
(length (st :messages))
|
||||||
(if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0")
|
(if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0")
|
||||||
(or (st :rule-count) 0)
|
(or (st :rule-count) 0)
|
||||||
(if (st :streaming-text) " [streaming]"
|
(if (st :streaming-text) " [streaming]"
|
||||||
(if (st :busy) " …thinking" "")))
|
(if (st :busy) " …thinking" "")))))
|
||||||
:y 1 :x 1 :fgcolor (theme-color (if (st :connected) :connected :disconnected)))
|
(cl-tty.backend:draw-text fb 1 1 line1
|
||||||
;; Second line: Focus map (left) + timestamp (right-aligned, v0.7.0)
|
(theme-color (if (st :connected) :connected :disconnected))
|
||||||
(let ((focus-info (or (st :foveal-id) "")))
|
nil)
|
||||||
(when (and focus-info (> (length focus-info) 0))
|
;; Second line: Focus map (left) + timestamp (right-aligned, v0.7.0)
|
||||||
(add-string win (format nil " [Focus: ~a]" focus-info)
|
(let ((focus-info (or (st :foveal-id) "")))
|
||||||
:y 2 :x 1 :fgcolor (theme-color :timestamp))))
|
(when (and focus-info (> (length focus-info) 0))
|
||||||
(add-string win (format nil " ~a" (now))
|
(cl-tty.backend:draw-text fb 1 2 (format nil " [Focus: ~a]" focus-info)
|
||||||
:y 2 :x (max 1 (- (width win) 12))
|
(theme-color :timestamp) nil)))
|
||||||
:fgcolor (theme-color :timestamp))
|
(cl-tty.backend:draw-text fb (max 1 (- w 12)) 2 (format nil " ~a" (now))
|
||||||
(refresh win))
|
(theme-color :timestamp) nil)))
|
||||||
|
|
||||||
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
|
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
|
||||||
(defun search-highlight (content query)
|
(defun search-highlight (content query)
|
||||||
@@ -85,11 +83,8 @@ that the TUI actuator attaches to the response plist before transmission.
|
|||||||
(setf result (concatenate 'string result (subseq content pos)))
|
(setf result (concatenate 'string result (subseq content pos)))
|
||||||
(if (string= result "") content result))))
|
(if (string= result "") content result))))
|
||||||
|
|
||||||
(defun view-chat (win h)
|
(defun view-chat (fb w h)
|
||||||
(clear win)
|
(let* ((msgs (st :messages))
|
||||||
(box win 0 0)
|
|
||||||
(let* ((w (or (width win) 78))
|
|
||||||
(msgs (st :messages))
|
|
||||||
(total (length msgs))
|
(total (length msgs))
|
||||||
(max-lines (- h 2))
|
(max-lines (- h 2))
|
||||||
(is-search (st :search-mode))
|
(is-search (st :search-mode))
|
||||||
@@ -101,7 +96,7 @@ that the TUI actuator attaches to the response plist before transmission.
|
|||||||
(query (st :search-query))
|
(query (st :search-query))
|
||||||
(header (format nil "Search: ~d matches for '~a' (~d/~d) — Esc to exit"
|
(header (format nil "Search: ~d matches for '~a' (~d/~d) — Esc to exit"
|
||||||
(length matches) query (1+ idx) (length matches))))
|
(length matches) query (1+ idx) (length matches))))
|
||||||
(add-string win header :y y :x 1 :n (1- w) :fgcolor (theme-color :highlight))
|
(cl-tty.backend:draw-text fb 1 y header (theme-color :highlight) nil)
|
||||||
(incf y)
|
(incf y)
|
||||||
(decf max-lines)))
|
(decf max-lines)))
|
||||||
;; Count visible messages from end, accounting for word wrap
|
;; Count visible messages from end, accounting for word wrap
|
||||||
@@ -110,14 +105,14 @@ that the TUI actuator attaches to the response plist before transmission.
|
|||||||
(loop for i from (1- total) downto 0
|
(loop for i from (1- total) downto 0
|
||||||
while (> lines-remaining 0)
|
while (> lines-remaining 0)
|
||||||
do (let* ((msg (aref msgs i))
|
do (let* ((msg (aref msgs i))
|
||||||
(role (getf msg :role))
|
(role (getf msg :role))
|
||||||
(content (getf msg :content))
|
(content (getf msg :content))
|
||||||
(time (or (getf msg :time) ""))
|
(time (or (getf msg :time) ""))
|
||||||
(prefix (case role (:user "⬆") (:agent "⬇") (t " ")))
|
(prefix (case role (:user "⬆") (:agent "⬇") (t " ")))
|
||||||
(content-show (if is-search
|
(content-show (if is-search
|
||||||
(search-highlight content (st :search-query))
|
(search-highlight content (st :search-query))
|
||||||
content))
|
content))
|
||||||
(line-text (format nil "~a [~a] ~a" prefix time content-show))
|
(line-text (format nil "~a [~a] ~a" prefix time content-show))
|
||||||
(wrapped (word-wrap line-text (- w 2)))
|
(wrapped (word-wrap line-text (- w 2)))
|
||||||
(nlines (length wrapped)))
|
(nlines (length wrapped)))
|
||||||
(if (<= nlines lines-remaining)
|
(if (<= nlines lines-remaining)
|
||||||
@@ -148,48 +143,40 @@ that the TUI actuator attaches to the response plist before transmission.
|
|||||||
(theme-color :hitl))))
|
(theme-color :hitl))))
|
||||||
(dolist (line wrapped)
|
(dolist (line wrapped)
|
||||||
(when (< y (1- h))
|
(when (< y (1- h))
|
||||||
(if (eq role :agent)
|
(cl-tty.backend:draw-text fb 1 y line color nil)
|
||||||
(let ((segments (parse-markdown-spans line)))
|
(incf y)))
|
||||||
(setf y (render-styled win segments y 1 w)))
|
|
||||||
(progn
|
|
||||||
(add-string win line :y y :x 1 :n (1- w) :fgcolor color)
|
|
||||||
(incf y)))))
|
|
||||||
;; v0.7.2: gate trace below agent messages
|
;; v0.7.2: gate trace below agent messages
|
||||||
(let ((gate-trace (getf msg :gate-trace)))
|
(let ((gate-trace (getf msg :gate-trace)))
|
||||||
(when (and gate-trace (not (member i (st :collapsed-gates))))
|
(when (and gate-trace (not (member i (st :collapsed-gates))))
|
||||||
(dolist (entry (passepartout::gate-trace-lines gate-trace))
|
(dolist (entry (passepartout::gate-trace-lines gate-trace))
|
||||||
(when (< y (1- h))
|
(when (< y (1- h))
|
||||||
(add-string win (car entry) :y y :x 3 :n (- w 4) :fgcolor (or (getf (cdr entry) :fgcolor) :dim))
|
(cl-tty.backend:draw-text fb 3 y (car entry)
|
||||||
(incf y))))))))))
|
(or (getf (cdr entry) :fgcolor) :dim) nil)
|
||||||
(refresh win))
|
(incf y)))))))))))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
** Input Line
|
** Input Line
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-view.lisp
|
||||||
(defun view-input (win)
|
(defun view-input (fb w)
|
||||||
(let* ((text (input-string))
|
(let* ((text (input-string))
|
||||||
(w (or (width win) 78))
|
|
||||||
(pos (or (st :cursor-pos) 0))
|
(pos (or (st :cursor-pos) 0))
|
||||||
(display-start (max 0 (- pos (1- w))))
|
(display-start (max 0 (- pos (1- w))))
|
||||||
(visible (subseq text display-start (min (length text) (+ display-start w)))))
|
(visible (subseq text display-start (min (length text) (+ display-start w)))))
|
||||||
(clear win)
|
(cl-tty.backend:draw-text fb 0 0 (format nil "~a " visible) (theme-color :input) nil)))
|
||||||
(add-string win (format nil "~a " visible) :y 0 :x 0 :n (1- w) :fgcolor (theme-color :input))
|
|
||||||
(setf (cursor-position win) (list 0 (min (- pos display-start) (1- w)))))
|
|
||||||
(refresh win))
|
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
** Redraw (dirty-flag dispatch)
|
** Redraw (dirty-flag dispatch)
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defun redraw (sw cw ch iw)
|
(defun redraw (fb w h)
|
||||||
(destructuring-bind (sd cd id) (st :dirty)
|
(destructuring-bind (sd cd id) (st :dirty)
|
||||||
(when sd (view-status sw))
|
(when sd (view-status fb w))
|
||||||
(when cd (view-chat cw ch))
|
(when cd (view-chat fb w (- h 5)))
|
||||||
(when id (view-input iw))
|
(when id (view-input fb w))
|
||||||
(setf (st :dirty) (list nil nil nil))))
|
(setf (st :dirty) (list nil nil nil))))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
* Implementation — v0.7.0 additions
|
* Implementation — v0.7.0 additions
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-view.lisp
|
||||||
(in-package :passepartout)
|
(in-package :passepartout)
|
||||||
|
|
||||||
(defun char-width (ch)
|
(defun char-width (ch)
|
||||||
@@ -213,10 +200,10 @@ ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8."
|
|||||||
((<= #x20D0 code #x20FF) 0)
|
((<= #x20D0 code #x20FF) 0)
|
||||||
((<= #xFE00 code #xFE0F) 0)
|
((<= #xFE00 code #xFE0F) 0)
|
||||||
(t 1))))
|
(t 1))))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
* v0.7.1 — Markdown Rendering
|
* v0.7.1 — Markdown Rendering
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-view.lisp
|
||||||
(in-package :passepartout)
|
(in-package :passepartout)
|
||||||
|
|
||||||
(defun parse-markdown-spans (text)
|
(defun parse-markdown-spans (text)
|
||||||
@@ -257,21 +244,20 @@ ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8."
|
|||||||
(t (push (cons (subseq text pos) nil) results) (return))))))))
|
(t (push (cons (subseq text pos) nil) results) (return))))))))
|
||||||
(nreverse results)))
|
(nreverse results)))
|
||||||
|
|
||||||
(defun render-styled (win segments y x w)
|
(defun render-styled (fb segments y x w)
|
||||||
"Render markdown segments to Croatoan window. Returns next y."
|
"Render markdown segments to cl-tty backend. Returns next y."
|
||||||
(dolist (seg segments)
|
(dolist (seg segments)
|
||||||
(when (>= y (height win)) (return y))
|
|
||||||
(let* ((text (or (car seg) ""))
|
(let* ((text (or (car seg) ""))
|
||||||
(attrs (cdr seg))
|
(attrs (cdr seg))
|
||||||
(bold (getf attrs :bold))
|
(bold (getf attrs :bold))
|
||||||
(code (getf attrs :code))
|
(code (getf attrs :code))
|
||||||
(underline (getf attrs :underline))
|
|
||||||
(url (getf attrs :url)))
|
(url (getf attrs :url)))
|
||||||
(add-string win text :y y :x x :n (max 1 (- w x))
|
(declare (ignore code))
|
||||||
:bold bold :underline underline
|
(cl-tty.backend:draw-text fb x y text
|
||||||
:bgcolor (when code (theme-color :dim))
|
(cond (url (theme-color :highlight))
|
||||||
:fgcolor (cond (url (theme-color :highlight))
|
(t (theme-color (or (getf attrs :role) :agent))))
|
||||||
(t (theme-color (or (getf attrs :role) :agent)))))
|
nil
|
||||||
|
:bold bold)
|
||||||
(incf x (length text))))
|
(incf x (length text))))
|
||||||
y)
|
y)
|
||||||
|
|
||||||
@@ -336,10 +322,10 @@ ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8."
|
|||||||
:keyword :function))) r)
|
:keyword :function))) r)
|
||||||
(setf p fe)))))))))
|
(setf p fe)))))))))
|
||||||
(nreverse r)))
|
(nreverse r)))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
* v0.7.2 — Gate Trace
|
* v0.7.2 — Gate Trace
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-view.lisp
|
||||||
(in-package :passepartout)
|
(in-package :passepartout)
|
||||||
|
|
||||||
(defun gate-trace-lines (trace)
|
(defun gate-trace-lines (trace)
|
||||||
@@ -366,10 +352,10 @@ ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8."
|
|||||||
(if (eq result :approval) " (HITL required)" ""))))
|
(if (eq result :approval) " (HITL required)" ""))))
|
||||||
(push (cons text (list :fgcolor color)) lines)))
|
(push (cons text (list :fgcolor color)) lines)))
|
||||||
(nreverse lines)))
|
(nreverse lines)))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|
||||||
* Test Suite
|
* Test Suite
|
||||||
#+begin_src lisp
|
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-view.lisp
|
||||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
(ql:quickload :fiveam :silent t))
|
(ql:quickload :fiveam :silent t))
|
||||||
|
|
||||||
@@ -477,4 +463,4 @@ ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8."
|
|||||||
(passepartout.channel-tui::init-state)
|
(passepartout.channel-tui::init-state)
|
||||||
(let ((cg (passepartout.channel-tui::st :collapsed-gates)))
|
(let ((cg (passepartout.channel-tui::st :collapsed-gates)))
|
||||||
(is (null cg))))
|
(is (null cg))))
|
||||||
#+end_src
|
#+END_SRC
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
(:file "lisp/core-pipeline")))
|
(:file "lisp/core-pipeline")))
|
||||||
|
|
||||||
(defsystem :passepartout/tui
|
(defsystem :passepartout/tui
|
||||||
:depends-on (:passepartout :croatoan :usocket :bordeaux-threads)
|
:depends-on (:passepartout :cl-tty :usocket :bordeaux-threads)
|
||||||
:serial t
|
:serial t
|
||||||
:components ((:file "lisp/channel-tui-state")
|
:components ((:file "lisp/channel-tui-state")
|
||||||
(:file "lisp/channel-tui-view")
|
(:file "lisp/channel-tui-view")
|
||||||
|
|||||||
Reference in New Issue
Block a user