Files
cl-tty/docs/ARCHITECTURE.org
Hermes db59fa4f55 v0.0.1: backend protocol — abstraction layer + simple backend
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
2026-05-11 12:45:26 +00:00

12 KiB

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

  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               │ │
  │  └─────────────┘  └─────────────────────────┘ │
  └───────────────────────────────────────────────┘

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

  ;; ── 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)

Style structure

All drawing functions accept a style plist that is resolved through the theme engine before reaching the backend:

  (: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

The backend receives resolved hex colors, not semantic roles. Theme resolution happens in the pipeline layer, before backend dispatch.

Backend Selection

At startup:

  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

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

  (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

Composable API

  (vbox (:gap 1 :padding 1)
    (header "Title")
    (hbox (:grow 1)
      (sidebar (:width 30) ...)
      (content ...)))

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

  (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

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

  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

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

  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

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)