Files
cl-tty/docs/ARCHITECTURE.org
Hermes Agent 47094c48e5 restructure: move backend/ and layout/ into src/; convert README to org syntax; fix demo package conflict and alien-sap ioctl; update ROADMAP with v0.15.0; remove stale files
- Move backend/ and layout/ directories into src/
- Update all path references in ASD, scripts, docs
- Convert README.org from Markdown syntax to proper Org-mode
- Fix demo.lisp use-package conflict (both backend and input export #:read-event)
- Fix modern-backend TIOCGWINSZ ioctl alien type (alien-sap wrapper)
- Add v0.15.0 section to ROADMAP, update line count to 5760
- Add known gaps (suspend/resume-backend, slot modes) to v1.0.0 checklist
- Remove docs/plans/, debug-layout.lisp, system-index.txt, ci-watchdog.sh
- Move tangle.py to Hermes skill (org-babel-tangle)
- Add .gitignore for fasl files
2026-05-12 16:57:19 +00:00

362 lines
14 KiB
Org Mode

#+TITLE: cl-tty Architecture
#+STARTUP: content
#+FILETAGS: :project:cl-tty:architecture:
* Architecture
cl-tty 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-tty/
├── cl-tty.asd # ASDF system (main + test)
├── README.org
├── LICENSE
├── .gitignore
├── demo.lisp # Interactive demo
├── demo.sh # PTY launcher for demo
├── run-all-tests.lisp # Test runner
├── docs/
│ ├── ROADMAP.org
│ └── ARCHITECTURE.org ← this file
├── org/ # Literate source files
│ ├── backend-protocol.org
│ ├── box-renderable.org
│ ├── detection.org
│ ├── dialog.org
│ ├── framebuffer.org
│ ├── layout-engine.org
│ ├── markdown-renderer.org
│ ├── modern-backend.org
│ ├── mouse.org
│ ├── scrollbox-tabbar.org
│ ├── select.org
│ ├── slot.org
│ └── text-input.org
├── src/
│ ├── backend/
│ │ ├── package.lisp
│ │ ├── classes.lisp
│ │ ├── simple.lisp
│ │ ├── modern.lisp
│ │ └── detection.lisp
│ ├── layout/
│ │ └── layout.lisp
│ ├── components/
│ │ ├── package.lisp
│ │ ├── box.lisp
│ │ ├── text.lisp
│ │ ├── render.lisp
│ │ ├── theme.lisp
│ │ ├── dirty.lisp
│ │ ├── input-package.lisp
│ │ ├── input.lisp
│ │ ├── text-input.lisp
│ │ ├── textarea.lisp
│ │ ├── keybindings.lisp
│ │ ├── container-package.lisp
│ │ ├── scrollbox.lisp
│ │ ├── tabbar.lisp
│ │ ├── select-package.lisp
│ │ ├── select.lisp
│ │ ├── markdown-package.lisp
│ │ ├── markdown.lisp
│ │ ├── dialog-package.lisp
│ │ ├── dialog.lisp
│ │ ├── mouse-package.lisp
│ │ ├── mouse.lisp
│ │ ├── slot-package.lisp
│ │ └── slot.lisp
│ └── rendering/
│ └── framebuffer.lisp
├── tests/
│ ├── input-tests.lisp
│ ├── scrollbox-tabbar-tests.lisp
│ ├── select-tests.lisp
│ ├── markdown-tests.lisp
│ ├── dialog-tests.lisp
│ ├── mouse-tests.lisp
│ ├── slot-tests.lisp
│ ├── framebuffer-tests.lisp
│ └── integration-tests.lisp
└── scripts/
├── binary-search.lisp
├── code-audit.lisp
├── audit-compiler.lisp
├── find-t-form.lisp
├── find-t-warning.lisp
└── verify-api.py
#+END_SRC
** Dependency Graph
src/backend/ (no deps)
src/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)