backend-protocol.org / simple.lisp: - Replace hard-coded 80x24 prose with full 5-step fallback chain (MY_TERM env vars → ioctl fd 0 → ioctl stdout → /dev/tty → 80x24) - Document return-from pattern (or discards secondary values) modern-backend.org / modern.lisp: - Replace simple ioctl-only prose with 4-step fallback chain - Document env-var pre-check and /dev/tty fallback text-input.org / input.lisp: - Update read-raw-byte prose: with-pinned-objects/vector-sap instead of alien buffer (code was already correct, prose stale) - Add missing (require :sb-posix) to SIGWINCH handler code block - Document :sb-posix requirement in prose
1035 lines
36 KiB
Org Mode
1035 lines
36 KiB
Org Mode
#+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, forces the stream to
|
|
flush, and returns the length of the string. Uses ~finish-output~ to
|
|
ensure the data is actually sent, which matters for pipe and network
|
|
output.
|
|
|
|
#+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)
|
|
(finish-output 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
|