Implement the backend protocol with two backends (modern planned, simple done). Includes package definitions, CLOS generic protocol, simple-backend with ASCII borders, and 9 FiveAM tests. RED: 9/9 tests failing (no implementation) GREEN: 9/9 tests passing - backend/package.lisp — defpackage, exports - backend/classes.lisp — backend base class, 18 generics - backend/simple.lisp — simple-backend implementation - backend/tests.lisp — 9 FiveAM test cases - org/backend-protocol.org — literate source
319 lines
12 KiB
Org Mode
319 lines
12 KiB
Org Mode
#+TITLE: cl-tui Architecture
|
|
#+STARTUP: content
|
|
#+FILETAGS: :project:cl-tui:architecture:
|
|
|
|
* Architecture
|
|
|
|
cl-tui is a layered framework. Each layer has a single responsibility
|
|
and communicates with adjacent layers through a well-defined protocol.
|
|
|
|
** Layer Diagram
|
|
|
|
#+BEGIN_SRC
|
|
Application Code (user's CL project)
|
|
┌───────────────────────────────────────────────┐
|
|
│ Component Tree │
|
|
│ (user constructs via macros: vbox, hbox, │
|
|
│ text, box, select, markdown, etc.) │
|
|
└──────────────┬────────────────────────────────┘
|
|
│ defgeneric render (component backend)
|
|
│ defgeneric handle-key (component event)
|
|
│ defgeneric handle-mouse (component event)
|
|
▼
|
|
┌───────────────────────────────────────────────┐
|
|
│ Rendering Pipeline │
|
|
│ 1. Layout pass (constraint solve) │
|
|
│ 2. Dirty walk (only changed branches) │
|
|
│ 3. Render commands (component → cmds) │
|
|
│ 4. Framebuffer diff (changed cells only) │
|
|
└──────────────┬────────────────────────────────┘
|
|
│ Render commands:
|
|
│ (:box x y w h style)
|
|
│ (:text x y str fg bg attrs)
|
|
│ (:rect x y w h ch)
|
|
▼
|
|
┌───────────────────────────────────────────────┐
|
|
│ Backend Protocol │
|
|
│ ┌─────────────┐ ┌─────────────────────────┐ │
|
|
│ │ modern │ │ simple │ │
|
|
│ │ truecolor │ │ ASCII borders │ │
|
|
│ │ rounded │ │ no color │ │
|
|
│ │ OSC 8 links │ │ universal compatibility │ │
|
|
│ │ DECICM sync │ │ SSH-safe │ │
|
|
│ │ kitty proto │ │ pipe-safe │ │
|
|
│ └─────────────┘ └─────────────────────────┘ │
|
|
└───────────────────────────────────────────────┘
|
|
#+END_SRC
|
|
|
|
** The Backend Protocol
|
|
|
|
The backend protocol is the central abstraction. Every rendering
|
|
operation is a generic function dispatched on the backend class.
|
|
|
|
*** Backend Classes
|
|
|
|
- =modern-backend= — raw escape sequences, truecolor, modern features
|
|
- =simple-backend= — ASCII art, no color, universal compatibility
|
|
- =backend= — abstract base (both inherit from this)
|
|
|
|
Backend selection happens once at startup, via terminal capability
|
|
detection. The same component tree renders correctly on both.
|
|
|
|
*** Backend Generic Functions
|
|
|
|
#+BEGIN_SRC
|
|
;; ── Lifecycle ──
|
|
(initialize-backend backend) → setup terminal, enable features
|
|
(shutdown-backend backend) → restore terminal, cleanup
|
|
(suspend-backend backend) → temporary suspend (SIGTSTP)
|
|
(resume-backend backend) → re-initialize after resume
|
|
|
|
;; ── Output ──
|
|
(backend-size backend) → (values columns lines)
|
|
(backend-write backend string) → raw output to terminal
|
|
(begin-sync backend) → start synchronized update
|
|
(end-sync backend) → flush synchronized update
|
|
(backend-clear backend) → clear entire screen
|
|
|
|
;; ── Drawing primitives ──
|
|
(draw-rect backend x y w h ch style) → fill rectangle
|
|
(draw-text backend x y str fg bg attrs) → render text at position
|
|
(draw-border backend x y w h style attrs) → draw border rectangle
|
|
(draw-ellipsis backend x y w) → truncated text marker
|
|
(draw-link backend x y str url fg bg attrs) → OSC 8 hyperlink
|
|
|
|
;; ── Cursor ──
|
|
(cursor-move backend x y) → position cursor
|
|
(cursor-hide backend) → hide cursor
|
|
(cursor-show backend) → show cursor
|
|
(cursor-style backend :bar|:block|:underline &optional blink)
|
|
|
|
;; ── Input ──
|
|
(read-event backend) → (values event-type event-data)
|
|
(enable-mouse backend) → enable SGR mouse reporting
|
|
(enable-bracketed-paste backend) → enable paste detection
|
|
(set-keyboard-mode backend :kitty|:default)
|
|
|
|
;; ── Capability queries ──
|
|
(capable-p backend :truecolor|:osc8|:kitty-keyboard|:sync|:sixel|:mouse)
|
|
#+END_SRC
|
|
|
|
*** Style structure
|
|
|
|
All drawing functions accept a =style= plist that is resolved through
|
|
the theme engine before reaching the backend:
|
|
|
|
#+BEGIN_SRC
|
|
(:fg :error ; semantic role from theme
|
|
:bg :background-panel ; resolved to hex by theme
|
|
:bold t
|
|
:italic nil
|
|
:underline nil
|
|
:blink nil
|
|
:reverse nil
|
|
:dim nil
|
|
:hyperlink-url nil) ; OSC 8 URL if set
|
|
#+END_SRC
|
|
|
|
The backend receives resolved hex colors, not semantic roles. Theme
|
|
resolution happens in the pipeline layer, before backend dispatch.
|
|
|
|
*** Backend Selection
|
|
|
|
At startup:
|
|
|
|
#+BEGIN_SRC
|
|
1. Check if stdout is a TTY (if not → simple-backend)
|
|
2. Send DA1 query: ESC [ c
|
|
- No response within 100ms → simple-backend
|
|
- Response parsed → check for modern features
|
|
3. Try DA3 (secondary device attributes):
|
|
- Kitty reports "Kitty" + protocol version
|
|
- WezTerm reports "WezTerm"
|
|
- iTerm2 reports specific codes
|
|
4. Query DECRPM for DECICM sync:
|
|
- ESC [?2026$p
|
|
- Response ESC [?2026;1$y = supported
|
|
5. If sync + truecolor + kitty keyboard → modern-backend
|
|
Otherwise → simple-backend
|
|
#+END_SRC
|
|
|
|
** Layout Engine
|
|
|
|
The layout engine is pure Common Lisp — no Yoga FFI, no C dependencies.
|
|
|
|
*** Constraint Model
|
|
|
|
A terminal has ~200x80 cells. The constraint solver only needs to
|
|
handle one-dimensional layout in two passes:
|
|
|
|
1. **Column direction (vertical pass):** distribute Y positions, sum
|
|
children heights. Width is inherited from parent (minus padding).
|
|
2. **Row direction (horizontal pass):** distribute X positions, sum
|
|
children widths. Height is inherited from parent.
|
|
|
|
Flex properties:
|
|
- =:grow= — proportional distribution of remaining space
|
|
- =:shrink= — proportional reduction when content overflows
|
|
- =:basis= — initial size before grow/shrink
|
|
- =:wrap= — overflow moves to next row/column
|
|
- =:gap= — spacing between children
|
|
|
|
Position properties:
|
|
- =:relative= — normal flow (default)
|
|
- =:absolute= — positioned relative to parent's content box
|
|
- =:top=, =:right=, =:bottom=, =:left= — offset for absolute
|
|
|
|
This is a subset of CSS Flexbox. Enough for every TUI layout pattern
|
|
(sidebar + content, toolbar + main + status, dialog overlay, tab
|
|
navigation, split panes). ~200 lines.
|
|
|
|
*** Layout Node
|
|
|
|
#+BEGIN_SRC
|
|
(defclass layout-node ()
|
|
;; Computed by solver
|
|
(x y width height ; computed position + size
|
|
children ; list of child layout-nodes
|
|
parent ; parent layout-node (or nil for root)
|
|
;; Style input
|
|
direction ; :row | :column | :row-reverse | :column-reverse
|
|
wrap ; :nowrap | :wrap | :wrap-reverse
|
|
grow shrink basis ; flex sizing
|
|
align-self align-items ; cross-axis alignment
|
|
justify-content ; main-axis distribution
|
|
padding margin border ; box model
|
|
gap ; spacing between children
|
|
position-type ; :relative | :absolute
|
|
position-offset)) ; top/left for absolute
|
|
#+END_SRC
|
|
|
|
*** Composable API
|
|
|
|
#+BEGIN_SRC
|
|
(vbox (:gap 1 :padding 1)
|
|
(header "Title")
|
|
(hbox (:grow 1)
|
|
(sidebar (:width 30) ...)
|
|
(content ...)))
|
|
#+END_SRC
|
|
|
|
Macros expand to layout-node construction + child wiring.
|
|
|
|
** Component Tree
|
|
|
|
Components are CLOS objects. Every component has a =layout-node=
|
|
slot that drives positioning. Components define =render= methods.
|
|
|
|
*** Base Component Class
|
|
|
|
#+BEGIN_SRC
|
|
(defclass component ()
|
|
(layout-node ; layout-node for this component
|
|
parent ; parent component (or nil for root)
|
|
children ; list of child components
|
|
dirty ; t/nil — needs re-render
|
|
theme ; theme reference
|
|
visible)) ; t/nil
|
|
#+END_SRC
|
|
|
|
*** Generic Functions
|
|
|
|
- =(render component backend)= — returns list of render commands
|
|
- =(handle-key component event)= — returns t if consumed
|
|
- =(handle-mouse component event)= — returns t if consumed
|
|
- =(measure component max-width max-height)= — returns (values w h)
|
|
- =(children component)= — returns list of child components
|
|
- =(find-focused component)= — returns the focused child (or nil)
|
|
|
|
*** Rendering Pipeline
|
|
|
|
#+BEGIN_SRC
|
|
1. (propagate-dirty root) → mark ancestors dirty
|
|
2. (compute-layout root w h) → pure CL constraint solve
|
|
3. (collect-commands root) → walk dirty branches, call render
|
|
4. (diff-framebuffer prev curr) → emit only changed cells
|
|
5. (begin-sync backend) → DECICM start
|
|
6. (flush-commands backend) → write escape sequences
|
|
7. (end-sync backend) → DECICM end
|
|
8. (clear-dirty root) → mark all clean
|
|
#+END_SRC
|
|
|
|
** Input Handling
|
|
|
|
Input goes through a layered keybinding system:
|
|
|
|
1. Terminal emits escape sequences → parser converts to events
|
|
2. Events dispatched through layers: =:global= → =:local= → =:focused=
|
|
3. Focused component's =handle-key= fires first
|
|
4. Unconsumed events bubble to =:local= keymap, then =:global=
|
|
5. Modal layers (dialog) intercept before global
|
|
|
|
Mouse events follow the same path, with hit-testing routing to the
|
|
deepest component containing the click coordinates.
|
|
|
|
** Theme Engine
|
|
|
|
Semantic tokens → hex colors → backend color pairs. No code references
|
|
hex values directly. =:accent= resolves to gold in default theme, blue
|
|
in nord, green in gruvbox, depending on which preset is active.
|
|
|
|
Presets define both =:dark= and =:light= variants. Auto-detection
|
|
reads terminal background color at startup.
|
|
|
|
** File Structure
|
|
|
|
#+BEGIN_SRC
|
|
cl-tui/
|
|
├── cl-tui.asd
|
|
├── cl-tui-tests.asd
|
|
├── README.org
|
|
├── LICENSE
|
|
├── docs/
|
|
│ ├── ROADMAP.org
|
|
│ └── ARCHITECTURE.org ← this file
|
|
├── src/
|
|
│ ├── package.lisp
|
|
│ ├── backend/
|
|
│ │ ├── protocol.lisp
|
|
│ │ ├── detection.lisp
|
|
│ │ ├── simple.lisp
|
|
│ │ └── modern.lisp
|
|
│ ├── layout/
|
|
│ │ ├── nodes.lisp
|
|
│ │ ├── solver.lisp
|
|
│ │ └── api.lisp
|
|
│ ├── components/
|
|
│ │ ├── base.lisp
|
|
│ │ ├── box.lisp
|
|
│ │ └── text.lisp
|
|
│ ├── rendering/
|
|
│ │ ├── pipeline.lisp
|
|
│ │ ├── dirty.lisp
|
|
│ │ └── diff.lisp
|
|
│ └── theme/
|
|
│ ├── tokens.lisp
|
|
│ └── presets.lisp
|
|
└── tests/
|
|
├── package.lisp
|
|
├── backend.lisp
|
|
├── layout.lisp
|
|
└── components.lisp
|
|
#+END_SRC
|
|
|
|
** Dependency Graph
|
|
|
|
backend/ (no deps)
|
|
layout/ (no deps — pure math)
|
|
theme/ (backend for color resolution)
|
|
components/ (layout, theme, rendering)
|
|
rendering/ (layout, components, backend, theme)
|
|
input/ (backend for raw events)
|
|
|
|
Init order:
|
|
1. Backend — detect, initialize
|
|
2. Theme — load default preset
|
|
3. Layout — construct component tree
|
|
4. Render — layout → commands → flush
|
|
5. Input — event loop (blocks on read-event)
|