#+TITLE: Plugin / Slot System (v0.11.0) #+DATE: 2026-05-11 #+AUTHOR: Amr Gharbeia / Hermes * 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 slot - ~slot-render slot-name &rest args~ — call all registered render-fns, return combined output - ~slot-p slot-name~ — check if a slot has registrations - ~clear-slot slot-name~ — remove all registrations for a slot - ~list-slots~ — return all slot names with registrations Slot modes: - ~:stack~ (default) — render all registered functions in ~:order~ sequence - ~: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. #+BEGIN_SRC lisp :tangle ../src/components/slot-package.lisp :noweb no (defpackage :cl-tty.slot (:use :cl) (:export #:defslot #:slot-render #:slot-p #:clear-slot #:list-slots #:*slots*)) #+END_SRC *** 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. #+BEGIN_SRC lisp :tangle ../src/components/slot.lisp :noweb no (in-package :cl-tty.slot) (defvar *slots* (make-hash-table :test #'equal) "Hash table mapping slot name (string) -> list of (order . render-fn) pairs.") #+END_SRC *** 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. #+BEGIN_SRC lisp :tangle ../src/components/slot.lisp :noweb no (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) #+END_SRC *** 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. #+BEGIN_SRC lisp :tangle ../src/components/slot.lisp :noweb no (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)))) #+END_SRC *** 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. #+BEGIN_SRC lisp :tangle ../src/components/slot.lisp :noweb no (defun slot-p (slot-name) (nth-value 1 (gethash (string slot-name) *slots*))) #+END_SRC *** 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. #+BEGIN_SRC lisp :tangle ../src/components/slot.lisp :noweb no (defun clear-slot (slot-name) (remhash (string slot-name) *slots*)) #+END_SRC *** 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. #+BEGIN_SRC lisp :tangle ../src/components/slot.lisp :noweb no (defun list-slots () (loop for key being the hash-keys of *slots* collect key)) #+END_SRC *** Tests The test suite uses FiveAM and exercises each public function. **** Test Package and Suite #+BEGIN_SRC lisp :tangle ../tests/slot-tests.lisp :noweb no (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) #+END_SRC **** defslot-register: Registering a slot makes it visible #+BEGIN_SRC lisp :tangle ../tests/slot-tests.lisp :noweb no (def-test defslot-register () (clear-slot :test-slot) (defslot :test-slot :order 1 :render-fn (lambda () "hello")) (is-true (slot-p :test-slot))) #+END_SRC **** slot-render-calls: Registered functions are called in order #+BEGIN_SRC lisp :tangle ../tests/slot-tests.lisp :noweb no (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)))) #+END_SRC **** slot-render-empty: Unregistered slot returns nil #+BEGIN_SRC lisp :tangle ../tests/slot-tests.lisp :noweb no (def-test slot-render-empty () (clear-slot :ghost) (is-false (slot-render :ghost))) #+END_SRC **** clear-slot-removes: Clearing a slot makes it absent #+BEGIN_SRC lisp :tangle ../tests/slot-tests.lisp :noweb no (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))) #+END_SRC