Files
cl-tty/org/slot.org
Hermes Agent 29f99a576d 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%.
2026-05-12 18:55:07 +00:00

6.0 KiB

Plugin / Slot System (v0.11.0)

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.

(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)))