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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user