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:
165
org/theme.org
165
org/theme.org
@@ -45,32 +45,75 @@ and the backend's ~*theme-colors*~ for SGR resolution.
|
||||
|
||||
* Tests
|
||||
|
||||
** Test header
|
||||
|
||||
Package declaration and test suite registration.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(in-package :cl-tty-box-test)
|
||||
(in-suite box-suite)
|
||||
#+END_SRC
|
||||
|
||||
** Test: theme-create-default
|
||||
|
||||
Verifies basic construction of a theme with default ~:dark~ mode. The
|
||||
~make-theme~ constructor should return an instance of the ~theme~
|
||||
class with ~:dark~ as the initial mode.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test theme-create-default
|
||||
"A theme can be created with default mode"
|
||||
(let ((th (make-theme)))
|
||||
(is (typep th 'theme))
|
||||
(is (eql (theme-mode th) :dark))))
|
||||
#+END_SRC
|
||||
|
||||
** Test: theme-create-light
|
||||
|
||||
Verifies explicit ~:light~ mode works. Both modes must produce themes
|
||||
ready to accept color role assignments.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test theme-create-light
|
||||
"A theme can be created in light mode"
|
||||
(let ((th (make-theme :mode :light)))
|
||||
(is (eql (theme-mode th) :light))))
|
||||
#+END_SRC
|
||||
|
||||
** Test: theme-color-set-and-get
|
||||
|
||||
Confirms ~setf~ on ~theme-color~ stores a value and that reading it
|
||||
back returns the same string. This is the core read/write contract
|
||||
for the theme's role map.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test theme-color-set-and-get
|
||||
"theme-color setf/get works"
|
||||
(let ((th (make-theme)))
|
||||
(setf (theme-color th :primary) "#FFD700")
|
||||
(is (string= (theme-color th :primary) "#FFD700"))))
|
||||
#+END_SRC
|
||||
|
||||
** Test: theme-color-unknown-returns-nil
|
||||
|
||||
Unassigned roles must return ~nil~ rather than signaling an error.
|
||||
This allows components to degrade gracefully when a theme doesn't
|
||||
define every possible role.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test theme-color-unknown-returns-nil
|
||||
"Unknown roles return nil"
|
||||
(let ((th (make-theme)))
|
||||
(is (null (theme-color th :nonexistent)))))
|
||||
#+END_SRC
|
||||
|
||||
** Test: load-default-dark-preset
|
||||
|
||||
Loading the ~:default~ preset in ~:dark~ mode must populate a set of
|
||||
expected roles with their documented hex values. We spot-check
|
||||
~:primary~, ~:background~, and ~:error~.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test load-default-dark-preset
|
||||
"Loading the default dark preset populates roles"
|
||||
(let ((th (make-theme :mode :dark)))
|
||||
@@ -78,27 +121,59 @@ and the backend's ~*theme-colors*~ for SGR resolution.
|
||||
(is (string= (theme-color th :primary) "#FFD700"))
|
||||
(is (string= (theme-color th :background) "#1A1A2E"))
|
||||
(is (string= (theme-color th :error) "#FF4444"))))
|
||||
#+END_SRC
|
||||
|
||||
** Test: load-default-light-preset
|
||||
|
||||
The light variant of ~:default~ must produce different values (warm
|
||||
tones on near-white). This validates the mode dispatch inside
|
||||
~load-preset~.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test load-default-light-preset
|
||||
"Light variant has different colors"
|
||||
(let ((th (make-theme :mode :light)))
|
||||
(load-preset th :default)
|
||||
(is (string= (theme-color th :primary) "#B8860B"))
|
||||
(is (string= (theme-color th :background) "#F8F9FA"))))
|
||||
#+END_SRC
|
||||
|
||||
** Test: load-nord-preset
|
||||
|
||||
The ~:nord~ preset must produce a distinct cool-blue palette,
|
||||
different from the ~:default~ gold scheme. This validates independent
|
||||
preset data.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test load-nord-preset
|
||||
"Nord preset has different colors than default"
|
||||
(let ((th (make-theme :mode :dark)))
|
||||
(load-preset th :nord)
|
||||
(is (string= (theme-color th :primary) "#88C0D0"))
|
||||
(is (string= (theme-color th :background) "#2E3440"))))
|
||||
#+END_SRC
|
||||
|
||||
** Test: load-preset-unknown-warns
|
||||
|
||||
An unknown preset name must signal a ~warning~ (not an ~error~) and
|
||||
leave the theme's roles unpopulated. This ensures graceful degradation
|
||||
when a preset is missing.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test load-preset-unknown-warns
|
||||
"Unknown preset warns but doesn't error"
|
||||
(let ((th (make-theme)))
|
||||
(signals warning (load-preset th :nonexistent))
|
||||
(is (null (theme-color th :primary)))))
|
||||
#+END_SRC
|
||||
|
||||
** Test: preset-switch-mode
|
||||
|
||||
Switching the mode at runtime and re-loading the same preset must
|
||||
produce the other variant's colors. This validates that ~load-preset~
|
||||
reads the current ~theme-mode~ each time, not a cached value.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme-tests.lisp
|
||||
(test preset-switch-mode
|
||||
"Switching mode and reloading changes colors"
|
||||
(let ((th (make-theme :mode :dark)))
|
||||
@@ -117,47 +192,84 @@ The ~theme~ class holds a mode flag (~:dark~/~:light~) and a hash
|
||||
table of role→hex mappings. The hash table gives O(1) lookups for
|
||||
~theme-color~ and clean iteration for ~load-preset~.
|
||||
|
||||
*** defclass theme
|
||||
|
||||
The class has two slots: ~mode~ (defaulting to ~:dark~, with an
|
||||
~:initarg~ and ~accessor~ for reads and writes) and ~roles~ (a hash
|
||||
table storing role→hex mappings, lazily initialized to an empty
|
||||
hash table). Using ~make-hash-table~ as the ~:initform~ ensures each
|
||||
instance gets its own table instead of sharing one.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme.lisp
|
||||
(in-package :cl-tty.box)
|
||||
|
||||
;; ── Theme Engine ──────────────────────────────────────────────
|
||||
|
||||
(defclass theme ()
|
||||
((mode :initform :dark :initarg :mode :accessor theme-mode)
|
||||
(roles :initform (make-hash-table) :accessor theme-roles)))
|
||||
#+END_SRC
|
||||
|
||||
*** defun make-theme
|
||||
|
||||
A convenience constructor that delegates to ~make-instance~. Wrapping
|
||||
this in a function lets us change the constructor signature without
|
||||
breaking callers. Mode defaults to ~:dark~, suitable for dark-background
|
||||
terminals; callers pass ~:mode :light~ for light backgrounds.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme.lisp
|
||||
(defun make-theme (&key (mode :dark))
|
||||
(make-instance 'theme :mode mode))
|
||||
#+END_SRC
|
||||
|
||||
The mode defaults to ~:dark~. Applications can initialize with
|
||||
~:light~ for terminals with light backgrounds. The mode controls
|
||||
which variant ~load-preset~ selects.
|
||||
|
||||
** Color resolution
|
||||
|
||||
*** defun theme-color
|
||||
|
||||
Reads a semantic role from the theme's roles hash table. Uses
|
||||
~gethash~ which returns ~nil~ for unknown roles — so missing roles
|
||||
degrade gracefully rather than crashing. The backend treats ~nil~ as
|
||||
"use default."
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme.lisp
|
||||
(defun theme-color (theme role)
|
||||
"Resolve a semantic ROLE to a hex color string in THEME."
|
||||
(gethash role (theme-roles theme)))
|
||||
#+END_SRC
|
||||
|
||||
*** defun (setf theme-color)
|
||||
|
||||
The setter companion to ~theme-color~. Storing via ~setf~ writes
|
||||
directly into the roles hash table. Uses ~setf~ on ~gethash~ which
|
||||
creates the entry if it doesn't exist.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme.lisp
|
||||
(defun (setf theme-color) (hex theme role)
|
||||
"Set the hex color for a semantic ROLE in THEME."
|
||||
(setf (gethash role (theme-roles theme)) hex))
|
||||
#+END_SRC
|
||||
|
||||
Uses ~gethash~ for both getter and setter. Unknown roles return ~nil~,
|
||||
which the backend treats as "use default" — so missing roles degrade
|
||||
gracefully rather than crashing.
|
||||
** Global preset registry
|
||||
|
||||
** Preset system
|
||||
A hash table (keyed by ~eq~-comparable keywords) stores all registered
|
||||
presets. Using ~#\\'~ (quoted list) instead of an alist or nested hash
|
||||
table keeps preset data inline and readable.
|
||||
|
||||
Presets are stored in a global hash table keyed by keyword name. The
|
||||
~define-preset~ macro registers a preset at macro-expansion time.
|
||||
*** defparameter *presets*
|
||||
|
||||
Global storage for preset definitions. The ~eq~ test matches keyword
|
||||
identity, which is the fastest hash test for keywords.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme.lisp
|
||||
(defparameter *presets* (make-hash-table :test #'eq))
|
||||
#+END_SRC
|
||||
|
||||
*** defmacro define-preset
|
||||
|
||||
Registers a preset by name (~keyword~) at macro-expansion time. The
|
||||
~check-type~ enforces that names are keywords. The macro expands to a
|
||||
~setf~ of ~gethash~, storing a plist of ~:dark~ and ~:light~ variants.
|
||||
Using a quoted list (not an alist or hash) keeps the data compact.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme.lisp
|
||||
(defmacro define-preset (name &key dark light)
|
||||
"Define a theme preset with DARK and LIGHT variants.
|
||||
NAME should be a keyword (e.g., :default, :nord)."
|
||||
@@ -165,9 +277,20 @@ NAME should be a keyword (e.g., :default, :nord)."
|
||||
`(setf (gethash ,name *presets*) '(:dark ,dark :light ,light)))
|
||||
#+END_SRC
|
||||
|
||||
Using ~#\'~ (quoted list) instead of an alist or hash table keeps the
|
||||
preset data inline and easy to read. The ~eq~ hash table test matches
|
||||
keyword identity.
|
||||
** Loading presets
|
||||
|
||||
*** defun load-preset
|
||||
|
||||
The central function that applies a named preset to a theme. Does
|
||||
double duty: populates the theme's role map and the backend's
|
||||
~*theme-colors*~. This second step is what makes semantic colors work
|
||||
at the SGR level — when the backend renders ~:accent~, it looks up
|
||||
~*theme-colors*~ to get the hex, then generates the escape sequence.
|
||||
|
||||
The ~loop for (role hex) on colors by #'cddr~ iterates the plist in
|
||||
pairs, setting both the theme entry and the backend entry. If the
|
||||
preset doesn't exist, ~warn~ is called instead of ~error~ — a missing
|
||||
preset shouldn't crash the application.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/theme.lisp
|
||||
(defun load-preset (theme preset-name)
|
||||
@@ -188,18 +311,6 @@ color roles resolve to hex at SGR generation time."
|
||||
(warn "Unknown preset: ~S" preset-name))))
|
||||
#+END_SRC
|
||||
|
||||
~load-preset~ does double duty: it populates the theme's role map and
|
||||
the backend's ~*theme-colors*~. This second step is what makes
|
||||
semantic colors work at the SGR level — when the backend renders
|
||||
~:accent~, it looks up ~*theme-colors*~ to get the hex, then
|
||||
generates the escape sequence.
|
||||
|
||||
The ~loop for (role hex) on colors by #'cddr~ iterates the plist in
|
||||
pairs, setting both the theme entry and the backend entry.
|
||||
|
||||
If the preset doesn't exist, ~warn~ is called instead of ~error~ — a
|
||||
missing preset shouldn't crash the application.
|
||||
|
||||
** Built-in presets
|
||||
|
||||
Two presets are built in:
|
||||
|
||||
Reference in New Issue
Block a user