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:
@@ -36,6 +36,9 @@ If detection can't determine modern capability, it falls back to
|
||||
- ~*detected-backend*~ — variable
|
||||
Cache for detection result. ~nil~ = not yet detected.
|
||||
|
||||
- ~query-terminal~ — function
|
||||
Low-level escape sequence query helper shared by probes.
|
||||
|
||||
* Plan
|
||||
|
||||
See =docs/plans/2026-05-11-terminal-detection.md= for implementation tasks.
|
||||
@@ -66,20 +69,36 @@ See =docs/plans/2026-05-11-terminal-detection.md= for implementation tasks.
|
||||
Detection functions are added to the existing ~cl-tty.backend~ package.
|
||||
No new package definition needed.
|
||||
|
||||
** Environment probe
|
||||
** Detection cache
|
||||
|
||||
Check ~COLORTERM~ first — it's the simplest and most reliable signal.
|
||||
The ~*detected-backend*~ special variable holds the cached backend instance
|
||||
after the first successful detection. Initializing it to ~nil~ gives downstream
|
||||
code a simple truthiness check — ~(or *detected-backend* ...)~ — so that
|
||||
~detect-backend~ returns immediately on re-entry without re-running probes.
|
||||
|
||||
Using a global variable rather than a closure or class slot keeps the detection
|
||||
path stateless and trivially resettable for testing: binding ~*detected-backend*~
|
||||
to ~nil~ forces a fresh detection run.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/backend/detection.lisp
|
||||
(in-package :cl-tty.backend)
|
||||
|
||||
;;; ─── Detection cache ────────────────────────────────────────────────────────
|
||||
|
||||
(defvar *detected-backend* nil
|
||||
"Cached backend instance from detect-backend. Nil = not yet detected.")
|
||||
#+END_SRC
|
||||
|
||||
;;; ─── Environment probe ──────────────────────────────────────────────────────
|
||||
** Environment probe
|
||||
|
||||
The ~COLORTERM~ environment variable is the single most reliable signal for
|
||||
truecolor support. It is set by modern terminal emulators (kitty, Alacritty,
|
||||
GNOME Terminal, iTerm2, Windows Terminal) and has near-zero false-positive
|
||||
rate. Checking it first avoids the I/O costs and race conditions of escape
|
||||
sequence queries.
|
||||
|
||||
Case-insensitive matching via ~char-equal~ handles variances across platforms
|
||||
(GNOME Terminal uses ~truecolor~, some Windows builds use ~24bit~).
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/backend/detection.lisp
|
||||
(defun detect-backend-by-env ()
|
||||
"Check COLORTERM environment variable for modern terminal support.
|
||||
Returns :modern if COLORTERM contains 'truecolor' or '24bit', nil otherwise."
|
||||
@@ -92,36 +111,36 @@ Returns :modern if COLORTERM contains 'truecolor' or '24bit', nil otherwise."
|
||||
|
||||
** TTY probe
|
||||
|
||||
Check if stdout is connected to a terminal (not a pipe or file).
|
||||
The ~interactive-stream-p~ function from the CL standard reliably distinguishes
|
||||
real terminals from pipes and redirected files. If stdout is not a terminal,
|
||||
escape sequence queries will hang or produce garbage, so this check gates all
|
||||
further (I/O-dependent) probes. Must happen before ~detect-backend-by-da1~.
|
||||
|
||||
Testing this predicate first also avoids wasting time on DA1 queries when the
|
||||
output is consumed by a test runner, CI pipeline, or pipe.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/backend/detection.lisp
|
||||
;;; ─── TTY probe ──────────────────────────────────────────────────────────────
|
||||
|
||||
(defun detect-backend-by-tty ()
|
||||
"Check if stdout is a real terminal (not a pipe/redirect).
|
||||
Returns T if stdout is interactive, nil otherwise."
|
||||
(interactive-stream-p *standard-output*))
|
||||
#+END_SRC
|
||||
|
||||
** DA1 terminal query (best-effort)
|
||||
** Low-level terminal query helper
|
||||
|
||||
Send a DA1 (Device Attributes) query and briefly listen for a response.
|
||||
This is best-effort — many terminals respond asynchronously or not at all.
|
||||
The ~query-terminal~ function encapsulates the mechanics of sending an escape
|
||||
sequence and collecting a response within a short timeout. Writing to
|
||||
~*standard-output*~ and reading from ~*standard-input*~ matches how terminal
|
||||
emulators actually deliver DA1/DA3 response bytes — they arrive on stdin, not
|
||||
on any query I/O stream. The original implementation used ~*query-io*~ for
|
||||
both sides, which silently failed on some emulators.
|
||||
|
||||
*** Bug Fixes (v1.0.0): query-terminal stream fix
|
||||
|
||||
~query-terminal~ originally used ~*query-io*~ for both writing the query and
|
||||
reading the response. In raw terminal mode, the terminal's response arrives on
|
||||
stdin, not on the query I/O stream. This caused ~query-terminal~ to never
|
||||
receive a response on certain terminal emulators.
|
||||
|
||||
Fix: Write queries to ~*standard-output*~ and read responses from
|
||||
~*standard-input*~. This matches where the terminal actually delivers its
|
||||
DA1/DA3 response bytes.
|
||||
Using ~listen~ in a polling loop with ~read-char-no-hang~ captures whatever
|
||||
bytes arrive within the timeout without blocking. The ~0.1~ second default
|
||||
strikes a balance between responsiveness and reliability: fast enough to avoid
|
||||
noticeable delay in interactive use, long enough for most terminals to reply.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/backend/detection.lisp
|
||||
;;; ─── DA1 terminal query ─────────────────────────────────────────────────────
|
||||
|
||||
(defun query-terminal (query &optional (timeout 0.1))
|
||||
"Send QUERY string to terminal and return any response received within
|
||||
TIMEOUT seconds. Returns the response string, or nil if no response."
|
||||
@@ -134,11 +153,26 @@ TIMEOUT seconds. Returns the response string, or nil if no response."
|
||||
do (vector-push-extend (read-char-no-hang *standard-input*) response))
|
||||
(when (plusp (length response))
|
||||
response)))
|
||||
#+END_SRC
|
||||
|
||||
** DA1 capability probe
|
||||
|
||||
The DA1 (Device Attributes) escape sequence (~ESC[c~) is an xterm-standard
|
||||
query that asks the terminal to report its feature set. Modern terminals
|
||||
(notably Kitty, which returns code 62) advertise their capabilities in the
|
||||
response. Searching for ~?62~ in the raw response is a heuristic — it targets
|
||||
Kitty's protocol extension identifier while being short enough to match
|
||||
variants across terminal implementations.
|
||||
|
||||
This probe is best-effort: many terminals do not respond within the timeout,
|
||||
and some return different codes for the same capabilities. A ~nil~ result from
|
||||
this function should never prevent fallback detection via environment variables.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/backend/detection.lisp
|
||||
(defun detect-backend-by-da1 ()
|
||||
"Send DA1 (ESC[c) query and check for kitty terminal response code.
|
||||
Returns T if terminal reports kitty compatibility codes."
|
||||
(let ((response (query-terminal (format nil "~C[c" #\Esc))))
|
||||
(let ((response (query-terminal (format nil "~C[c" (code-char 27)))))
|
||||
(when response
|
||||
;; DA1 response format: ESC [ ? digits ; digits c
|
||||
;; Kitty reports code 62 in the response
|
||||
@@ -147,11 +181,19 @@ Returns T if terminal reports kitty compatibility codes."
|
||||
|
||||
** Orchestrator
|
||||
|
||||
Tie all probes together into ~detect-backend~.
|
||||
The ~detect-backend~ function ties all probes together with a short-circuit
|
||||
caching strategy. On first call, it:
|
||||
|
||||
1. Checks if stdout is a real TTY (fast, gates all I/O)
|
||||
2. Checks ~COLORTERM~ (fast, most reliable signal)
|
||||
3. Falls back to DA1 query (slow, best-effort, skipped if env check passed)
|
||||
|
||||
The ~and~ / ~or~ structure naturally short-circuits: if ~detect-backend-by-tty~
|
||||
returns nil, the expensive DA1 query never runs. If ~detect-backend-by-env~
|
||||
returns ~:modern~, the DA1 query is skipped. The result is cached in
|
||||
~*detected-backend*~ so subsequent calls are effectively free.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/backend/detection.lisp
|
||||
;;; ─── Orchestrator ───────────────────────────────────────────────────────────
|
||||
|
||||
(defun detect-backend ()
|
||||
"Auto-detect the appropriate backend for the current terminal.
|
||||
Returns a backend instance (modern-backend or simple-backend).
|
||||
|
||||
Reference in New Issue
Block a user