#+TITLE: cl-tty Backend Protocol — v0.0.1 #+STARTUP: content #+FILETAGS: :cl-tty:backend:v0.0.1: #+OPTIONS: ^:nil * Overview The backend protocol is the rendering abstraction layer. Every visual operation dispatches through generic functions on a backend class. Two implementations exist: ~modern-backend~ (raw escape sequences, truecolor, modern terminal features) and ~simple-backend~ (ASCII art, universal compatibility). All drawing operations are generic functions dispatched on the backend class. Application code never calls terminal escape sequences directly. * Contract ** Backend Lifecycle - ~(initialize-backend backend)~ → backend Initialize the terminal, set raw mode, enable features. Returns the backend instance. - ~(shutdown-backend backend)~ → nil Restore terminal to cooked mode, reset colors, show cursor. Must be called on exit regardless of how the image stops. - ~(backend-size backend)~ → (values columns lines) Return terminal dimensions. First value = columns, second = lines. - ~(backend-write backend string)~ → integer Write raw string to terminal output. Returns number of bytes written. - ~(backend-clear backend)~ → nil Clear the entire screen and reset cursor to (0,0). ** Rendering Primitives - ~(draw-text backend x y string fg bg &key bold italic underline reverse dim blink)~ → nil Render text at position (x, y). fg and bg are hex color strings (e.g. "#FFD700") or nil for default. Attributes are booleans. - ~(draw-border backend x y width height &key style fg bg title title-align)~ → nil Draw a border rectangle. Style is :single, :double, or :rounded. - ~(draw-rect backend x y width height &key bg)~ → nil Fill a rectangle with background color. - ~(draw-link backend x y string url &key fg bg)~ → nil Render clickable hyperlink (OSC 8 escape sequence). - ~(draw-ellipsis backend x y width &key fg bg)~ → nil Render "..." truncated text marker at position. ** Cursor Operations - ~(cursor-move backend x y)~ → nil - ~(cursor-hide backend)~ → nil - ~(cursor-show backend)~ → nil - ~(cursor-style backend shape &key blink)~ → nil Shape is :block, :bar, or :underline. ** Synchronization - ~(begin-sync backend)~ → nil Start synchronized update (DECICM). All subsequent output is buffered by the terminal until ~end-sync~. - ~(end-sync backend)~ → nil Flush synchronized update buffer. The entire frame appears at once. ** Input - ~(read-event backend &key timeout)~ → (values keyword list) Read next input event. Blocks until event or timeout. - ~(enable-mouse backend)~ → nil Enable SGR mouse tracking. - ~(enable-bracketed-paste backend)~ → nil Enable bracketed paste mode. ** Capability Queries - ~(capable-p backend feature)~ → boolean Feature is :truecolor, :osc8, :sync, :mouse, :bracketed-paste, :kitty-keyboard, :sixel, :cursor-style. ** Backend Classes - ~(make-simple-backend &key output-stream)~ → simple-backend Minimal backend. ASCII borders, no color, no modern features. - ~(make-modern-backend &key output-stream)~ → modern-backend Full-featured backend. Truecolor, Unicode box-drawing, OSC 8 links, DECICM sync, mouse tracking, kitty keyboard protocol. * Tests The test suite is organized around the backend protocol contract. Each rendering primitive and lifecycle operation has a dedicated test case. Tests use a capturing backend (a simple-backend wired to a string output stream) so assertions check actual output strings rather than terminal behavior. ** Test Package and Suite FiveAM requires a test package with :use of :fiveam and the system under test. The suite name ~backend-suite~ is referenced by the multi-suite runner in ~run-all-tests.lisp~. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (defpackage :cl-tty-backend-test (:use :cl :fiveam :cl-tty.backend) (:export #:run-tests)) (in-package :cl-tty-backend-test) (def-suite backend-suite :description "Backend protocol tests") (in-suite backend-suite) #+END_SRC ** Capturing Backend Helper Tests need to inspect what the backend actually writes. This helper creates a simple-backend pointed at a string output stream and returns both the backend and the stream. The test can then call ~get-output-stream-string~ after the operation. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (defun make-capturing-backend () "Create a simple-backend that writes to a string stream." (let* ((s (make-string-output-stream)) (b (make-simple-backend :output-stream s))) (values b s))) #+END_SRC ** Test Runner Entry Point The ~run-tests~ function is an alternative entry point for interactive use or for downstream scripts that want to run only the backend suite. It prints results with FiveAM's explainer. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (defun run-tests () "Run all backend tests." (let ((result (run 'backend-suite))) (fiveam:explain! result) (uiop:quit 0))) #+END_SRC ** Simple Backend Lifecycle Verifies that a simple-backend can be constructed, initialized, and shut down without errors. Also confirms that the capability query returns nil for truecolor — the defining characteristic of the simple backend. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test simple-backend-lifecycle "simple-backend can be created and shut down" (let ((b (make-simple-backend))) (is (typep b 'simple-backend)) (initialize-backend b) (is-false (capable-p b :truecolor) "simple backend has no truecolor") (is (null (multiple-value-list (suspend-backend b)))) (is (null (multiple-value-list (resume-backend b)))) (shutdown-backend b))) #+END_SRC ** Simple Backend Draw Text The simple backend ignores style attributes (bold, italic, color) and position. It merely appends the text string to the output stream. This test confirms that passing style keywords does not change the output — the captured stream should contain exactly the string "hello". #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test simple-backend-draw-text "simple-backend renders text at position, ignoring style" (multiple-value-bind (b s) (make-capturing-backend) (initialize-backend b) (draw-text b 0 0 "hello" :red nil :bold t :italic t) (shutdown-backend b) (is (string= (get-output-stream-string s) "hello") "draw-text should output the string ignoring style"))) #+END_SRC ** Simple Backend Draw Border Border rendering on the simple backend uses ASCII characters: ~+~ for corners, ~-~ for horizontal edges, ~|~ for vertical edges. This test checks that the top edge contains "+---+" and a middle row shows "| |" with pipe-separated empty space. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test simple-backend-draw-border "simple-backend draws ASCII border with +-| characters" (multiple-value-bind (b s) (make-capturing-backend) (initialize-backend b) (draw-border b 0 0 5 3 :style :single) (shutdown-backend b) (let ((out (get-output-stream-string s))) (is (search "+---+" out) "top edge should have +---+\"") (is (search "| |" out) "middle row should have pipe sides")))) #+END_SRC ** Simple Backend Draw Rounded Border The simple backend does not support rounded corners — every style falls back to the same ASCII characters. This test verifies that requesting ~:rounded~ produces the same output as ~:single~, confirming the graceful fallback. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test simple-backend-draw-rounded "simple-backend falls back to straight edges for rounded style" (multiple-value-bind (b s) (make-capturing-backend) (initialize-backend b) (draw-border b 0 0 5 3 :style :rounded) (shutdown-backend b) (let ((out (get-output-stream-string s))) ;; Rounded falls back to ASCII -- identical output to single (is (search "+---+" out) "rounded style produces same dashes as single")))) #+END_SRC ** Simple Backend Draw Link Hyperlinks are meaningless on a simple terminal output. The simple backend's ~draw-link~ should output only the visible text and completely ignore the URL parameter. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test simple-backend-draw-link "simple-backend renders link as plain text" (multiple-value-bind (b s) (make-capturing-backend) (initialize-backend b) (draw-link b 0 0 "click me" "http://example.com") (shutdown-backend b) (is (string= (get-output-stream-string s) "click me") "simple-backend ignores URL, outputs text only"))) #+END_SRC ** Simple Backend Draw Ellipsis Truncation markers are rendered as three literal dots on the simple backend. This test checks that ~draw-ellipsis~ outputs exactly "..." at the specified position. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test simple-backend-draw-ellipsis "simple-backend renders ... for ellipsis" (multiple-value-bind (b s) (make-capturing-backend) (initialize-backend b) (draw-ellipsis b 0 0 5) (shutdown-backend b) (is (string= (get-output-stream-string s) "...") "ellipsis should output 3 dots"))) #+END_SRC ** Capability Query: Known Features All known terminal features should report ~nil~ on the simple backend. This comprehensive check iterates every feature keyword to ensure the simple backend makes no false claims about its capabilities. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test capable-p-known-features "capable-p returns nil for all features on simple-backend" (let ((b (make-simple-backend))) (initialize-backend b) (dolist (f '(:truecolor :osc8 :sync :mouse :bracketed-paste :kitty-keyboard :sixel :cursor-style)) (is-false (capable-p b f) (format nil "~s should not be supported on simple-backend" f))) (shutdown-backend b))) #+END_SRC ** Backend Size Returns Integers The ~backend-size~ function must return two integer values representing columns and lines. This test verifies the return types and a minimum size threshold (10 columns, 3 lines) for any terminal-like environment. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test backend-size-returns-integers "backend-size returns two integer values" (let ((b (make-simple-backend))) (initialize-backend b) (multiple-value-bind (cols lines) (backend-size b) (is (integerp cols)) (is (integerp lines)) (is (>= cols 10)) (is (>= lines 3))) (shutdown-backend b))) #+END_SRC ** Default Methods Are No-Ops All cursor operations and sync operations on the default backend should return ~nil~ (or ~(values)~) without signaling errors. This test calls ~cursor-hide~, ~cursor-show~, ~cursor-style~, ~begin-sync~, and ~end-sync~ and confirms none of them produce multiple return values. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test default-methods-are-no-ops "Default backend methods don't error" (let ((b (make-simple-backend))) (initialize-backend b) (is (null (multiple-value-list (cursor-hide b)))) (is (null (multiple-value-list (cursor-show b)))) (is (null (multiple-value-list (cursor-style b :block)))) (is (null (multiple-value-list (begin-sync b)))) (is (null (multiple-value-list (end-sync b)))) (shutdown-backend b))) #+END_SRC ** Sync Is No-Op on Simple Synchronized updates (DECICM) have no meaning on a simple terminal output. This test verifies that wrapping a draw-text call between ~begin-sync~ and ~end-sync~ produces exactly the same output as draw-text alone — no extra escape sequences are emitted. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test sync-is-noop-on-simple "begin-sync and end-sync produce no output on simple-backend" (multiple-value-bind (b s) (make-capturing-backend) (initialize-backend b) (begin-sync b) (draw-text b 0 0 "in sync" nil nil) (end-sync b) (shutdown-backend b) (is (string= (get-output-stream-string s) "in sync") "no sync escape sequences should appear"))) #+END_SRC ** Draw Rect Is No-Op on Simple Background fill operations require escape sequences to change cell colors. Since the simple backend emits no escape sequences, ~draw-rect~ should produce zero output regardless of the fill color requested. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test draw-rect-fills-area-correctly "draw-rect with background writes nothing to output (simple-backend no-op)" (multiple-value-bind (b s) (make-capturing-backend) (initialize-backend b) (draw-rect b 0 0 5 3 :bg :red) (shutdown-backend b) (is (string= (get-output-stream-string s) "") "draw-rect is a no-op on simple-backend"))) #+END_SRC ** Backend Detection Returns Instance The ~detect-backend~ function must return a backend instance suitable for the current environment. This test verifies that the returned value is of type ~backend~ (satisfying the protocol). #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test detection-returns-backend-instance "detect-backend returns a valid backend instance" (let ((be (cl-tty.backend:detect-backend))) (is (typep be 'cl-tty.backend:backend)))) #+END_SRC ** Backend Detection Caches Result ~detect-backend~ caches its result in ~*detected-backend*~ so that subsequent calls are cheap. This test clears the cache, calls detect-backend, and verifies that the special variable is no longer nil. #+BEGIN_SRC lisp :tangle ../src/backend/tests.lisp (test detection-caches-result "detect-backend caches the result in *detected-backend*" (let ((*detected-backend* nil)) (cl-tty.backend:detect-backend) (is-true (not (null cl-tty.backend::*detected-backend*))))) #+END_SRC * Implementation This section defines the base backend protocol and the simple backend implementation. Each function, generic function, and method is documented individually with its design rationale. ** Package The ~cl-tty.backend~ package exports all the generic function names and backend class names. It uses only ~:cl~ — no external dependencies. The package also exports internal symbols (~sgr-fg~, ~hex-to-24bit~, etc.) for testing. These let the test suite verify escape sequence construction without actually rendering to a terminal. #+BEGIN_SRC lisp :tangle ../src/backend/package.lisp (defpackage :cl-tty.backend (:use :cl) (:export ;; Backend classes #:backend #:simple-backend ;; Lifecycle #:initialize-backend #:shutdown-backend #:suspend-backend #:resume-backend #:backend-size #:backend-write #:backend-clear ;; Drawing #:draw-text #:draw-border #:draw-rect #:draw-link #:draw-ellipsis ;; Cursor #:cursor-move #:cursor-hide #:cursor-show #:cursor-style ;; Sync #:begin-sync #:end-sync ;; Input #:read-event #:enable-mouse #:enable-bracketed-paste ;; Queries #:capable-p ;; Constructors #:make-simple-backend #:with-terminal ;; Modern backend #:modern-backend #:make-modern-backend ;; Detection #:detect-backend #:*detected-backend* ;; Theme color resolution (populated by theme system) #:*theme-colors* ;; Internal (for testing) #:sgr-fg #:sgr-bg #:sgr-attr #:cursor-move-escape #:cursor-style-escape #:decicm-begin #:decicm-end #:osc8-link #:hex-to-rgb #:border-char)) (in-package :cl-tty.backend) #+END_SRC ** Backend Base Class The ~backend~ class itself is empty — it's a base for method dispatch. Every generic function on ~backend~ has a default method so that new backend implementations only need to override the functions they actually support. *** Backend Class Definition An empty base class. There are no slots because backends manage their own state (e.g., output streams) directly. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (in-package :cl-tty.backend) (defclass backend () ()) #+END_SRC *** Initialize Backend Sets up terminal raw mode and enables features. The default method returns the backend instance unchanged — subclasses that need setup override this. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric initialize-backend (backend) (:method ((b backend)) b)) #+END_SRC *** Shutdown Backend Restores terminal to cooked mode, resets colors, shows cursor. Must be called on exit. The default method is a no-op returning multiple values; subclasses with terminal state override this. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric shutdown-backend (backend) (:method ((b backend)) (values))) #+END_SRC *** Backend Size Returns terminal dimensions as two values: columns and lines. The default of 80x24 is a safe fallback that works everywhere. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric backend-size (backend) (:method ((b backend)) (values 80 24))) #+END_SRC *** Backend Write Writes a raw string to the terminal output. Has no default method because every backend must provide its own output mechanism — there is no reasonable universal behavior. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric backend-write (backend string)) #+END_SRC *** Backend Clear Clears the entire screen and resets the cursor to (0,0). The default method sends the ANSI escape sequence ~ESC[2J~ (clear entire screen) followed by ~ESC[H~ (cursor home). #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric backend-clear (backend) (:method ((b backend)) (backend-write b (format nil "~C[2J~C[H" #\Esc #\Esc)))) #+END_SRC *** Draw Text Renders text at position (x, y) with foreground and background colors and style attributes. The ~&allow-other-keys~ is important: it lets individual backend methods accept keyword arguments they don't use without signaling an error. The simple backend ignores styles; the modern backend processes them. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric draw-text (backend x y string fg bg &key bold italic underline reverse dim blink &allow-other-keys)) #+END_SRC *** Draw Border Draws a border rectangle with optional title. Style is one of ~:single~, ~:double~, or ~:rounded~. The default method has no implementation — each backend provides its own border rendering. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric draw-border (backend x y width height &key style fg bg title title-align)) #+END_SRC *** Draw Rectangle Fills a rectangular area with a background color. On the simple backend this is a no-op; on the modern backend it writes space characters with the appropriate SGR background color. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric draw-rect (backend x y width height &key bg)) #+END_SRC *** Draw Link Renders a clickable hyperlink using OSC 8 escape sequences. The default is a protocol declaration only — modern-backend implements the actual escape sequences, simple-backend falls back to plain text. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric draw-link (backend x y string url &key fg bg)) #+END_SRC *** Draw Ellipsis Renders a "..." truncation marker at position (x, y). This is used when text exceeds the available width. Each backend positions the marker according to its own coordinate system. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric draw-ellipsis (backend x y width &key fg bg)) #+END_SRC *** Cursor Move Moves the cursor to absolute position (x, y). The default method is a no-op — backends that support cursor positioning override this. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric cursor-move (backend x y) (:method ((b backend) x y) (declare (ignore x y)) (values))) #+END_SRC *** Cursor Hide Hides the terminal cursor. The default method is a no-op so that backends that lack cursor control still work safely. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric cursor-hide (backend) (:method ((b backend)) (values))) #+END_SRC *** Cursor Show Shows the terminal cursor after a hide. Always paired with ~cursor-hide~. Default is a no-op. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric cursor-show (backend) (:method ((b backend)) (values))) #+END_SRC *** Cursor Style Sets the cursor shape and blink behavior. Shape is ~:block~, ~:bar~, or ~:underline~. Default is a no-op for backends that don't support cursor styling. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric cursor-style (backend shape &key blink) (:method ((b backend) shape &key blink) (values))) #+END_SRC *** Begin Sync Starts a synchronized update (DECICM). All subsequent output is buffered by the terminal until ~end-sync~. Default is a no-op. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric begin-sync (backend) (:method ((b backend)) (values))) #+END_SRC *** End Sync Flushes the synchronized update buffer so the entire frame appears at once. Always paired with ~begin-sync~. Default is a no-op. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric end-sync (backend) (:method ((b backend)) (values))) #+END_SRC *** Read Event Reads the next input event from the terminal. Blocks until an event arrives or the timeout expires. Returns (values keyword event-data). The default method returns ~(values nil nil)~ — no events available. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric read-event (backend &key timeout) (:method ((b backend) &key timeout) (values nil nil))) #+END_SRC *** Enable Mouse Enables SGR mouse tracking so mouse click and motion events are reported as input. Default is a no-op on backends that don't support mouse input. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric enable-mouse (backend) (:method ((b backend)) (values))) #+END_SRC *** Enable Bracketed Paste Enables bracketed paste mode so the application can distinguish pasted text from typed input. Default is a no-op. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric enable-bracketed-paste (backend) (:method ((b backend)) (values))) #+END_SRC *** Capable-P Feature Query Queries whether the backend supports a named feature. Feature keywords include ~:truecolor~, ~:osc8~, ~:sync~, ~:mouse~, ~:bracketed-paste~, ~:kitty-keyboard~, ~:sixel~, and ~:cursor-style~. The default method returns ~nil~ for all features. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (defgeneric capable-p (backend feature) (:method ((b backend) feature) (declare (ignore feature)) nil)) #+END_SRC *** Suspend and Resume Temporary terminal suspension and re-initialization. Used when the application receives SIGTSTP (suspend) or SIGCONT (resume) signals. The default methods are no-ops; backends with terminal state override these to restore cooked mode on suspend and raw mode on resume. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (in-package :cl-tty.backend) (defgeneric suspend-backend (backend) (:documentation "Temporarily suspend the backend, restoring terminal to normal state. Called before SIGTSTP or similar suspension. Application should redraw after resume.") (:method ((b backend)) (values))) (defgeneric resume-backend (backend) (:documentation "Re-initialize the backend after suspension. Called after SIGCONT or similar resume. Re-enables raw mode and backend features.") (:method ((b backend)) (values))) #+END_SRC *** With Terminal A convenience macro that initializes a terminal backend, executes body, and guarantees cleanup on exit via ~unwind-protect~. The macro detects a suitable backend, initializes it, captures the terminal dimensions, binds them to the provided variables, executes the body, and always calls ~shutdown-backend~ when the body exits (whether normally or by a non-local control transfer). Arguments: - ~backend-var~ — bound to the detected backend instance. - ~cols-var~, ~rows-var~ (optional) — bound to terminal columns and lines captured after initialization. - ~&body body~ — executed with the above bindings. #+BEGIN_SRC lisp :tangle ../src/backend/classes.lisp (in-package :cl-tty.backend) (defmacro with-terminal ((backend-var &optional cols-var rows-var) &body body) "Execute BODY with a fully initialized terminal backend. DETECT-BACKEND, INITIALIZE-BACKEND, and SHUTDOWN-BACKEND are called automatically. The backend instance is bound to BACKEND-VAR. If COLS-VAR and ROWS-VAR are provided, they are bound to the terminal dimensions at startup. The caller should wrap this in SB-POSIX:WITH-RAW-TERMINAL (or equivalent) if raw-mode input handling is needed. Example: (with-terminal (be cols rows) (loop for ev = (read-event be :timeout 0.1) while ev do (format t \"~A~%\" ev))))" (let ((be-sym (gensym "BE")) (c-sym (gensym "COLS")) (r-sym (gensym "ROWS"))) `(let* ((,be-sym (detect-backend)) ,@(when cols-var `((,c-sym (nth-value 0 (backend-size ,be-sym))))) ,@(when rows-var `((,r-sym (nth-value 1 (backend-size ,be-sym)))))) (initialize-backend ,be-sym) (unwind-protect (let ((,backend-var ,be-sym) ,@(when cols-var `((,cols-var ,c-sym))) ,@(when rows-var `((,rows-var ,r-sym)))) ,@body) (shutdown-backend ,be-sym))))) #+END_SRC ** Simple Backend ~simple-backend~ inherits from ~backend~ and implements every operation in pure ASCII. No escape sequences, no color, no modern features. Works in any terminal, pipe, or serial connection. *** Simple Backend Class The ~simple-backend~ class has a single slot: ~output-stream~. This defaults to ~*standard-output*~ but can be overridden via the ~:output-stream~ initarg — the key extensibility point. Tests use ~make-string-output-stream~ to capture output, while production uses ~*standard-output*~. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (in-package :cl-tty.backend) (defclass simple-backend (backend) ((output-stream :initform *standard-output* :initarg :output-stream :accessor backend-output-stream))) #+END_SRC *** Make Simple Backend Constructor function that creates a ~simple-backend~ instance. Uses ~make-instance~ with the provided output stream or falls back to ~*standard-output*~. This function is exported rather than exposing ~make-instance~ directly to provide a stable API surface. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defun make-simple-backend (&key output-stream) (make-instance 'simple-backend :output-stream (or output-stream *standard-output*))) #+END_SRC *** Initialize Backend (Simple) Simple backend initialization is a no-op — there is no terminal state to configure. Returns the backend instance to satisfy the protocol contract. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod initialize-backend ((b simple-backend)) b) #+END_SRC *** Shutdown Backend (Simple) Simple backend shutdown is a no-op — there is no terminal state to restore. Returns multiple values to satisfy the protocol contract. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod shutdown-backend ((b simple-backend)) (values)) #+END_SRC *** Suspend (simple-backend) No-op — simple backend has no terminal state to save. #+begin_src lisp :tangle ../src/backend/simple.lisp (defmethod suspend-backend ((b simple-backend)) (values)) #+end_src *** Resume (simple-backend) No-op — simple backend has no terminal state to restore. #+begin_src lisp :tangle ../src/backend/simple.lisp (defmethod resume-backend ((b simple-backend)) (values)) #+end_src *** Backend Size (Simple) Queries actual terminal dimensions through a fallback chain, with a hard-coded 80x24 at the end: 1. **Env var pre-check** — ~MY_TERM_COLS~ / ~MY_TERM_ROWS~, set by the calling script before ~exec sbcl~. Checked with ~return-from~ so that /both/ values are preserved (Common Lisp's ~or~ discards secondary values). 2. **ioctl on fd 0 (stdin)** — the parent's real terminal fd. 3. **ioctl on stdout** — fast and correct after SIGWINCH at runtime. 4. **ioctl on ~/dev/tty~** — fallback when stdin/stdout are pipes. 5. **~(values 80 24)~** — last resort. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod backend-size ((b simple-backend)) ;; MY_TERM_COLS/MY_TERM_ROWS — set by the calling script. ;; Check with return-from to preserve both values. (let ((cstr (sb-ext:posix-getenv "MY_TERM_COLS")) (rstr (sb-ext:posix-getenv "MY_TERM_ROWS"))) (when (and cstr rstr) (let ((cols (parse-integer cstr :junk-allowed t)) (rows (parse-integer rstr :junk-allowed t))) (when (and cols rows (> cols 0) (> rows 0)) (return-from backend-size (values cols rows)))))) (or ;; ioctl on fd 0 (stdin) — the parent's own terminal. (multiple-value-bind (cols rows) (ignore-errors (let ((winsize (sb-alien:make-alien sb-alien:unsigned-short 4))) (unwind-protect (let ((ok (sb-unix:unix-ioctl 0 21523 (sb-alien:alien-sap winsize)))) (when ok (let ((c (sb-alien:deref winsize 1)) (r (sb-alien:deref winsize 0))) (when (and c r (> c 0) (> r 0)) (values c r))))) (sb-alien:free-alien winsize)))) (when (and cols rows (> cols 0) (> rows 0)) (values cols rows))) ;; ioctl on stdout fd — fast, correct after SIGWINCH at runtime. (multiple-value-bind (cols rows) (ignore-errors (let* ((winsize (sb-alien:make-alien sb-alien:unsigned-short 4))) (unwind-protect (let ((ok (sb-unix:unix-ioctl (sb-sys:fd-stream-fd (backend-output-stream b)) 21523 (sb-alien:alien-sap winsize)))) (when ok (values (sb-alien:deref winsize 1) (sb-alien:deref winsize 0)))) (sb-alien:free-alien winsize)))) (when (and cols rows (> cols 0) (> rows 0)) (values cols rows))) (ignore-errors (let ((tty-fd (sb-unix:unix-open "/dev/tty" 0 0))) (when (and tty-fd (numberp tty-fd) (> tty-fd 0)) (unwind-protect (let* ((winsize (sb-alien:make-alien sb-alien:unsigned-short 4))) (let ((ok (sb-unix:unix-ioctl tty-fd 21523 (sb-alien:alien-sap winsize)))) (when ok (values (sb-alien:deref winsize 1) (sb-alien:deref winsize 0)))) (sb-alien:free-alien winsize)) (sb-unix:unix-close tty-fd))))) (values 80 24))) #+END_SRC *** Backend Write (Simple) Writes a string to the backend's output stream and returns its length. Does NOT flush — explicit sync points (~initialize-backend~, ~end-sync~, etc.) call ~finish-output~ as needed. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod backend-write ((b simple-backend) string) (let ((stream (backend-output-stream b))) (write-string string stream) (length string))) #+END_SRC *** Draw Text (Simple) The simple backend's ~draw-text~ ignores position, color, and style completely. It appends only the string content to the output stream. This means simple backends are always a "scroll and dump" mode — no cursor positioning, no escape sequences. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod draw-text ((b simple-backend) x y string fg bg &key bold italic underline reverse dim blink &allow-other-keys) (declare (ignore x y fg bg bold italic underline reverse dim blink)) (backend-write b string)) #+END_SRC *** Simple Border Character Helper Returns the ASCII character for a given border position. All four corners use ~#\+~, horizontal edges use ~#\-~, and vertical edges use ~#\|~. No style distinction — single, double, and rounded are identical in ASCII output. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defun %simple-border-char (pos) "Return ASCII border character at POS. POS is :top-left, :top-right, :bottom-left, :bottom-right, :horizontal, or :vertical." (case pos ((:top-left :top-right :bottom-left :bottom-right) #\+) (:horizontal #\-) (:vertical #\|))) #+END_SRC *** Draw Border (Simple) Draws a border using only newlines and spaces for positioning — no escape sequences. This makes it compatible with pipe output. The title rendering supports ~:left~ and ~:center~ alignment, placing the title inside the top border line with horizontal dashes filling the remaining space. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod draw-border ((b simple-backend) x y width height &key style fg bg title title-align) (declare (ignore style fg bg)) (let ((h (%simple-border-char :horizontal)) (v (%simple-border-char :vertical)) (tl (%simple-border-char :top-left)) (tr (%simple-border-char :top-right)) (bl (%simple-border-char :bottom-left)) (br (%simple-border-char :bottom-right))) ;; Position cursor with newlines and spaces (no escape sequences) (dotimes (row y) (backend-write b (string #\Newline))) ;; Top edge with optional title (backend-write b (make-string x :initial-element #\space)) (backend-write b (string tl)) (if (and title (plusp (length title))) (let* ((align (or title-align :left)) (inner-width (- width 2)) (max-tlen (- inner-width 2)) (tlen (min (length title) max-tlen)) (trunc-title (subseq title 0 tlen))) (ecase align (:left (backend-write b (string #\Space)) (backend-write b trunc-title) (backend-write b (string #\Space)) (backend-write b (make-string (- inner-width tlen 2) :initial-element h))) (:center (let* ((total-pad (- inner-width tlen)) (left-pad (floor total-pad 2)) (right-pad (- total-pad left-pad))) (backend-write b (make-string left-pad :initial-element h)) (backend-write b trunc-title) (backend-write b (make-string right-pad :initial-element h)))))) (backend-write b (make-string (- width 2) :initial-element h))) (backend-write b (string tr)) ;; Sides (loop for i from 1 below (1- height) do (backend-write b (string #\Newline)) (backend-write b (make-string x :initial-element #\space)) (backend-write b (string v)) (backend-write b (make-string (- width 2) :initial-element #\space)) (backend-write b (string v))) ;; Bottom edge (backend-write b (string #\Newline)) (backend-write b (make-string x :initial-element #\space)) (backend-write b (string bl)) (backend-write b (make-string (- width 2) :initial-element h)) (backend-write b (string br)))) #+END_SRC *** Draw Rect (Simple) Background fill is impossible without escape sequences. This method is a no-op — it discards all arguments and returns ~(values)~. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod draw-rect ((b simple-backend) x y width height &key bg) (declare (ignore x y width height bg)) ;; On simple backend, background fill is a no-op (values)) #+END_SRC *** Draw Link (Simple) Hyperlinks fall back to plain text on the simple backend. The URL parameter is discarded entirely; the visible text is rendered via ~draw-text~ with no styling. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod draw-link ((b simple-backend) x y string url &key fg bg) (declare (ignore url fg bg)) (draw-text b x y string nil nil)) #+END_SRC *** Draw Ellipsis (Simple) Renders "..." using the simple backend's positioning pattern: newlines to reach the target row, spaces to reach the target column, then the literal three dots. No escape sequences are used. #+BEGIN_SRC lisp :tangle ../src/backend/simple.lisp (defmethod draw-ellipsis ((b simple-backend) x y width &key fg bg) (declare (ignore width fg bg)) ;; Position using newlines+spaces (simple-backend pattern) (dotimes (row y) (backend-write b (string #\Newline))) (backend-write b (make-string x :initial-element #\Space)) (backend-write b "...")) #+END_SRC