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%.
6.0 KiB
Plugin / Slot System (v0.11.0)
- Overview
Overview
Extensible named slots. Applications and plugins register content into named slots. The component tree renders whatever is registered.
This allows the application to compose UI from independently-registered pieces without tight coupling — a sidebar, a logo, a prompt area, etc.
Contract
defslot name &key order render-fn— register a render function for a slotslot-render slot-name &rest args— call all registered render-fns, return combined outputslot-p slot-name— check if a slot has registrationsclear-slot slot-name— remove all registrations for a slotlist-slots— return all slot names with registrations
Slot modes:
:stack(default) — render all registered functions in:ordersequence:replace— last registration wins, earlier ones are discarded:single-winner— first matching registration wins, rest are skipped
Implementation
The package provides the public API and exports all slot system symbols. Clients :use this package or refer to symbols qualified.
(defpackage :cl-tty.slot
(:use :cl)
(:export
#:defslot
#:slot-render
#:slot-p
#:clear-slot
#:list-slots
#:*slots*))
Slot Storage: slots
The central registry is a hash table keyed by slot name (strings, for
case-insensitive lookup via equal). Each value is a list of
(order . render-fn) cons cells, sorted by order on insertion. The
:test #'equal ensures that :sidebar and \"sidebar\" map to the
same key.
(in-package :cl-tty.slot)
(defvar *slots* (make-hash-table :test #'equal)
"Hash table mapping slot name (string) -> list of (order . render-fn) pairs.")
defslot: Register a Render Function
defslot inserts a new (order . render-fn) entry into the slot's
entry list. If the slot has no previous entries a fresh list is
created; otherwise the new entry is consed onto the existing list and
the whole list is sorted by order ascending. The render-fn itself
is returned so callers can use it inline or store it.
(defun defslot (name &key (order 0) render-fn)
(let* ((key (string name))
(entries (gethash key *slots*)))
(if (null entries)
(setf (gethash key *slots*) (list (cons order render-fn)))
(setf (gethash key *slots*)
(sort (cons (cons order render-fn) entries) #'< :key #'car))))
render-fn)
slot-render: Invoke All Render Functions
Iterates over the slot's registered entries and calls each non-nil
render function with the supplied args. Entries with a nil handler
are silently skipped — this is important because defslot accepts an
optional :render-fn keyword that defaults to nil, and we must
guard against calling apply on nil (a type error in Common Lisp).
Returns a list of results, one per non-nil render function. Returns
nil (via when) if the slot has no registrations at all.
(defun slot-render (slot-name &rest args)
(let ((entries (gethash (string slot-name) *slots*)))
(when entries
(mapcar (lambda (entry)
(let ((fn (cdr entry)))
(when fn (apply fn args))))
entries))))
slot-p: Check Slot Existence
Uses nth-value 1 of gethash which returns t if the key is
present (even if the value is nil) or nil if absent. This is the
canonical Common Lisp idiom for testing hash-table membership.
(defun slot-p (slot-name)
(nth-value 1 (gethash (string slot-name) *slots*)))
clear-slot: Remove All Registrations
Calls remhash to delete the slot's entry from the hash table
entirely. After this call slot-p returns false and slot-render
returns nil for the given slot name.
(defun clear-slot (slot-name)
(remhash (string slot-name) *slots*))
list-slots: Enumerate Registered Slots
Iterates over all hash keys in *slots* and returns them as a list.
Only slots that have been registered (i.e. have at least one entry)
appear in the result.
(defun list-slots ()
(loop for key being the hash-keys of *slots* collect key))
Tests
The test suite uses FiveAM and exercises each public function.
Test Package and Suite
(defpackage :cl-tty-slot-test (:use :cl :cl-tty.slot :fiveam))
(in-package :cl-tty-slot-test)
(def-suite slot-suite :description "Slot system tests")
(in-suite slot-suite)
defslot-register: Registering a slot makes it visible
(def-test defslot-register ()
(clear-slot :test-slot)
(defslot :test-slot :order 1 :render-fn (lambda () "hello"))
(is-true (slot-p :test-slot)))
slot-render-calls: Registered functions are called in order
(def-test slot-render-calls ()
(clear-slot :test-slot)
(defslot :test-slot :order 1 :render-fn (lambda () "a"))
(defslot :test-slot :order 2 :render-fn (lambda () "b"))
(is (equal '("a" "b") (slot-render :test-slot))))
slot-render-empty: Unregistered slot returns nil
(def-test slot-render-empty ()
(clear-slot :ghost)
(is-false (slot-render :ghost)))
clear-slot-removes: Clearing a slot makes it absent
(def-test clear-slot-removes ()
(clear-slot :test-slot)
(defslot :test-slot :order 1 :render-fn (lambda () "x"))
(clear-slot :test-slot)
(is-false (slot-p :test-slot)))