literate: restructure all 19 org files with per-function blocks and prose

Every function, defclass, defstruct, defgeneric, defmethod, defmacro,
defvar, and defparameter in every org file now has its own #+BEGIN_SRC
block with literate prose above it explaining the design reasoning.

Block counts before → after:
  package.org:           1 → 7
  container-package.org: 1 → 1 (prose expanded)
  dirty.org:             4 → 6
  render.org:           10 → 25
  theme.org:             6 → 19
  box-renderable.org:    9 → 29
  scrollbox.org:         8 → 26
  tabbar.org:            5 → 10
  backend-protocol.org:  8 → 66
  modern-backend.org:   17 → 53
  detection.org:         4 → 6
  layout-engine.org:     9 → 36
  framebuffer.org:       8 → 37
  markdown-renderer.org:13 → 38
  dialog.org:           17 → 23 (merged dual structure)
  mouse.org:             4 → 25
  select.org:           12 → 30
  slot.org:              4 → 12
  text-input.org:       11 → 53

Total: ~153 blocks → ~502 blocks

Bugs fixed during restructuring:
- render.org: stray π character typo (backenπd → backend)
- modern-backend.org: sgr-attr missing closing paren + #+END_SRC
- detection.org: invalid #\Esc character reference
- select.org: extra closing paren in select-visible-options

All 13 test suites pass at 100%.
This commit is contained in:
Hermes Agent
2026-05-12 18:55:07 +00:00
parent 927f786716
commit 29f99a576d
42 changed files with 4730 additions and 1745 deletions

View File

@@ -37,6 +37,12 @@ carry a ~layout-node~ for position/size computed by the layout engine.
* Tests
** Package and test suite setup
The test package exports ~run-tests~ so it can be invoked from the
top-level test runner. ~fiveam~ imports directly for declarative
~test~ and ~is~ forms. The ~box-suite~ collects all box/text tests.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(defpackage :cl-tty-box-test
(:use :cl :fiveam :cl-tty.backend :cl-tty.layout :cl-tty.box)
@@ -45,25 +51,54 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(def-suite box-suite :description "Box renderable tests")
(in-suite box-suite)
#+END_SRC
** Test runner entry point
~run-tests~ is the entry point called from the top-level
~run-all-tests.lisp~. It runs the ~box-suite~, explains results to
stdout, and exits cleanly with ~uiop:quit~.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(defun run-tests ()
(let ((result (run 'box-suite)))
(fiveam:explain! result)
(uiop:quit 0)))
#+END_SRC
** Capturing backend helper
~make-capturing-backend~ creates a backend that writes to a
~string-output-stream~ so tests can inspect rendered output without
actual terminal I/O. Returns the backend and stream as multiple
values.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(defun make-capturing-backend ()
(let* ((s (make-string-output-stream))
(b (make-modern-backend :output-stream s)))
(values b s)))
#+END_SRC
;; ── Box Tests ─────────────────────────────────────────────────
** Test: box-creates-with-defaults
Verify that a bare ~make-box~ returns a ~box~ instance and
automatically creates a ~layout-node~ through inheritance.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test box-creates-with-defaults
"A box created with no arguments has reasonable defaults"
(let ((b (make-box)))
(is (typep b 'box))
(is (typep (box-layout-node b) 'layout-node))))
#+END_SRC
** Test: box-renders-border
Verify that a box with ~:border-style :single~ draws the four corner
characters (┌ ┐ └ ┘) in the output stream.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test box-renders-border
"A box with border draws border characters"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -75,7 +110,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(is (search "┐" out) "top-right corner")
(is (search "└" out) "bottom-left corner")
(is (search "┘" out) "bottom-right corner")))))
#+END_SRC
** Test: box-renders-background
Verify that a box with ~:bg :red~ emits SGR background color codes
(41m) in the output stream.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test box-renders-background
"A box with background color fills interior"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -85,7 +127,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(let ((out (get-output-stream-string s)))
(is (search "┌" out) "border with background")
(is (search "41m" out) "SGR background for red")))))
#+END_SRC
** Test: box-renders-title
Verify that a title string appears in the rendered output stream
when ~:title~ is provided.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test box-renders-title
"A box with title renders the title text"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -94,7 +143,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(render-box bx b)
(let ((out (get-output-stream-string s)))
(is (search "Hello" out) "title text should appear")))))
#+END_SRC
** Test: box-without-border
Verify that ~:border-style nil~ suppresses corner characters but
background fill rendering continues to work.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test box-without-border
"A box with border-style nil draws no border"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -104,7 +160,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(let ((out (get-output-stream-string s)))
(is (search "41m" out) "background still renders")
(is-false (search "┌" out) "no top-left corner")))))
#+END_SRC
** Test: box-zero-size
Verify that a box with zero width and height produces no output
(triggers the early-return guard in ~render-box~).
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test box-zero-size
"A box with any zero dimension renders nothing"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -113,7 +176,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(render-box bx b)
(is (string= (get-output-stream-string s) "")
"zero-size box produces no output"))))
#+END_SRC
** Test: box-single-column
Verify that a box with width 1 produces no output — ~draw-border~
requires at least 2 columns to draw corner and edge characters.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test box-single-column
"A box with width 1 renders nothing (needs min 2 for border)"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -122,7 +192,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(render-box bx b)
(is (string= (get-output-stream-string s) "")
"width=1 box renders nothing"))))
#+END_SRC
** Test: box-minimum-size
Verify that a 2x2 box (the minimum viable size for border rendering)
still produces corner characters in the output.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test box-minimum-size
"A box with minimum non-zero size still renders"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -131,15 +208,27 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(render-box bx b)
(let ((out (get-output-stream-string s)))
(is (search "┌" out) "2x2 box still has borders")))))
#+END_SRC
;; ── Text and Span Tests ───────────────────────────────────────
** Test: text-creates-with-defaults
Verify that ~make-text~ with an empty string returns a ~text~
instance and creates a ~layout-node~.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test text-creates-with-defaults
"A text created with no arguments has reasonable defaults"
(let ((txt (make-text "")))
(is (typep txt 'text))
(is (typep (text-layout-node txt) 'layout-node))))
#+END_SRC
** Test: text-renders-content
Verify that text content appears in the captured output stream after
rendering.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test text-renders-content
"A text renders its content at position"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -148,7 +237,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(render-text tx b)
(let ((out (get-output-stream-string s)))
(is (search "Hello" out) "content should appear")))))
#+END_SRC
** Test: text-empty-string
Verify that an empty string produces no output (triggers the
early-return guard in ~render-text~).
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test text-empty-string
"Empty text produces no output"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -157,7 +253,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(render-text tx b)
(is (string= (get-output-stream-string s) "")
"empty string produces no output"))))
#+END_SRC
** Test: text-truncates-when-no-wrap
Verify that ~:wrap-mode :none~ truncates the content string to fit
within the available width, producing only the first N characters.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test text-truncates-when-no-wrap
"Text with wrap-mode :none truncates at width"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -167,7 +270,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(render-text tx b)
(let ((out (get-output-stream-string s)))
(is (search "Hello" out) "truncated to first 5 chars")))))
#+END_SRC
** Test: text-word-wraps
Verify that ~:wrap-mode :word~ breaks lines at word boundaries,
distributing words across successive rows.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test text-word-wraps
"Text with wrap-mode :word wraps at word boundaries"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -178,7 +288,14 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(is (search "Hello" out) "first line")
(is (search "brave" out) "second line")
(is (search "new" out) "third line")))))
#+END_SRC
** Test: text-word-wrap-single-word
Verify that a single word longer than the available width is
hard-broken at character boundaries into ~max-width~-sized chunks.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test text-word-wrap-single-word
"A word longer than width is hard-broken at max-width"
(multiple-value-bind (b s) (make-capturing-backend)
@@ -188,14 +305,28 @@ carry a ~layout-node~ for position/size computed by the layout engine.
(let ((out (get-output-stream-string s)))
(is (search "Hel" out) "first chunk is Hel")
(is (search "lo" out) "second chunk is lo")))))
#+END_SRC
** Test: span-creates-with-attributes
Verify that ~span~ stores its text content and style attributes
correctly, with unset attributes defaulting to ~nil~.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test span-creates-with-attributes
"A span has text and optional style attributes"
(let ((s (span "bold text" :bold t)))
(is (string= (span-text s) "bold text"))
(is-true (span-bold s))
(is-false (span-italic s))))
#+END_SRC
** Test: make-text-with-spans
Verify that ~make-text~ with ~:spans~ stores the provided span
objects and they are accessible via ~text-spans~.
#+BEGIN_SRC lisp :tangle ../src/components/box-tests.lisp
(test make-text-with-spans
"Text with spans stores span objects"
(let* ((sp (list (span "Hello" :bold t)
@@ -212,7 +343,8 @@ carry a ~layout-node~ for position/size computed by the layout engine.
~box~ inherits from ~dirty-mixin~ so changes (resize, title update,
color change) trigger incremental re-render. The ~layout-node~ slot
holds the computed position and size from the layout engine.
holds the computed position and size from the layout engine. Border
style, title, alignment, and colors are all configurable slots.
#+BEGIN_SRC lisp :tangle ../src/components/box.lisp
(in-package :cl-tty.box)
@@ -229,8 +361,11 @@ holds the computed position and size from the layout engine.
(bg :initform nil :initarg :bg :accessor box-bg)))
#+END_SRC
** make-box constructor
The constructor wraps ~make-instance~ and passes layout parameters
through to the layout node:
through to the layout node. Width and height are optional; when
omitted the layout engine will compute them from parent constraints.
#+BEGIN_SRC lisp :tangle ../src/components/box.lisp
(defun make-box (&key (border-style :single) title
@@ -248,9 +383,15 @@ through to the layout node:
:direction :column)))
#+END_SRC
** render-box function
~render-box~ draws the border at the component's layout position.
It handles zero-size (returns immediately) and optional background
fill.
fill. The early return for ~(< w 2)~ is important: ~draw-border~
requires at least 2 columns of width to draw corner characters.
Title rendering supports ~:left~, ~:center~, and ~:right~ alignment
with automatic truncation when the title is wider than available
content area (width-4 when border is present).
#+BEGIN_SRC lisp :tangle ../src/components/box.lisp
(defun render-box (box backend)
@@ -282,20 +423,16 @@ fill.
(t (draw-text backend tx ty display fg bg))))))))
#+END_SRC
The early return for ~(< w 2)~ is important: ~draw-border~ requires
at least 2 columns of width to draw corner characters.
** Span class
~span~ represents an inline styled segment within a Text component.
Multiple spans let a single Text contain bold, colored, or italicized
runs.
runs. Each style attribute is a separate slot so consumers can
inspect and apply them individually.
#+BEGIN_SRC lisp :tangle ../src/components/text.lisp
(in-package :cl-tty.box)
;; ── Text Renderable ────────────────────────────────────────────
(defclass span ()
((text :initarg :text :accessor span-text)
(bold :initform nil :initarg :bold :accessor span-bold)
@@ -305,7 +442,15 @@ runs.
(dim :initform nil :initarg :dim :accessor span-dim)
(fg :initform nil :initarg :fg :accessor span-fg)
(bg :initform nil :initarg :bg :accessor span-bg)))
#+END_SRC
** span constructor
~span~ is a convenience function for creating ~span~ instances with
keyword arguments for all style attributes. A ~nil~ default means
"inherit/no-change" when merged with parent styling context.
#+BEGIN_SRC lisp :tangle ../src/components/text.lisp
(defun span (text &key bold italic underline reverse dim fg bg)
(make-instance 'span
:text text :bold bold :italic italic
@@ -316,8 +461,9 @@ runs.
** Text class
~text~ renders a string at a layout position with word-wrapping.
Spans are stored but not yet rendered with per-run styling in the
current implementation.
Spans are stored for future per-run styling but the current
implementation renders all content as plain text. It inherits from
~dirty-mixin~ so content, color, or size changes trigger re-render.
#+BEGIN_SRC lisp :tangle ../src/components/text.lisp
(defclass text (dirty-mixin)
@@ -328,7 +474,16 @@ current implementation.
(fg :initform nil :initarg :fg :accessor text-fg)
(bg :initform nil :initarg :bg :accessor text-bg)
(wrap-mode :initform :word :initarg :wrap-mode :accessor text-wrap-mode)))
#+END_SRC
** make-text constructor
~make-text~ is a convenience constructor that accepts layout
dimensions and content parameters. It defaults ~wrap-mode~ to ~:word~
so text wraps by default, and creates a ~:column~-oriented layout
node.
#+BEGIN_SRC lisp :tangle ../src/components/text.lisp
(defun make-text (content &key fg bg wrap-mode width height spans)
(make-instance 'text
:content content
@@ -339,9 +494,13 @@ current implementation.
:width width :height height)))
#+END_SRC
** render-text function
~render-text~ handles both wrap modes. For ~:word~, it calls
~word-wrap~ to break the content into lines, then renders each line
at successive row positions.
at successive row positions. For ~:none~, it truncates the content to
fit the width in a single line. Empty content or zero dimensions
triggers an early return.
#+BEGIN_SRC lisp :tangle ../src/components/text.lisp
(defun render-text (text-object backend)
@@ -373,7 +532,8 @@ at successive row positions.
~word-wrap~ implements the line-breaking algorithm. It splits the
input into words, then packs them into lines respecting ~max-width~.
Words that exceed ~max-width~ are hard-broken at character boundaries.
Words that exceed ~max-width~ are hard-broken at character boundaries
in chunks of ~max-width~ to ensure no line exceeds the limit.
#+BEGIN_SRC lisp :tangle ../src/components/text.lisp
(defun word-wrap (text max-width)
@@ -405,7 +565,12 @@ Words that exceed ~max-width~ are hard-broken at character boundaries.
(or (nreverse lines) (list "")))))
#+END_SRC
~split-string~ tokenizes on whitespace (space, tab, newline):
** split-string utility
~split-string~ tokenizes on whitespace characters (space, tab,
newline). It uses ~position-if~ to find delimiters and builds the
word list iteratively. Consecutive delimiters are collapsed
(only one advance per delimiter character).
#+BEGIN_SRC lisp :tangle ../src/components/text.lisp
(defun split-string (string)