Rename: cl-tty avoids naming collision with Quicklisp's cl-tui (naryl/cl-tui, a cl-charms-based ncurses library). Our project is pure escape-sequence CL. v0.9.0 adds: - Dialog base class: modal overlay with backdrop, centered panel, size variants (:small/:medium/:large), stack-based management - Dialog subclasses: alert, confirm, select-dialog, prompt-dialog - Toast notifications: transient, top-right corner, auto-dismiss, colored variants (info/success/warning/error) - 78 tests total, 100% passing ASDF: read-time package references (+fiveam:+) replaced with find-symbol so .asd loads without FiveAM pre-loaded
383 lines
11 KiB
Org Mode
383 lines
11 KiB
Org Mode
#+TITLE: cl-tty Backend Protocol — v0.0.1
|
|
#+STARTUP: content
|
|
#+FILETAGS: :cl-tty:backend:v0.0.1:
|
|
#+OPTIONS: ^:nil
|
|
|
|
* Backend Protocol
|
|
|
|
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).
|
|
|
|
** 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 integer integer)
|
|
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
|
|
Move cursor to position (x, y). Origin is top-left (0,0).
|
|
|
|
- =(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.
|
|
Returns event type keyword and event data plist.
|
|
|
|
- =(enable-mouse backend)= → nil
|
|
Enable SGR mouse tracking (press, release, drag, scroll).
|
|
|
|
- =(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
|
|
|
|
*** Simple Backend
|
|
|
|
=(make-simple-backend)= → simple-backend
|
|
|
|
The minimal backend. ASCII borders, no color, no modern features.
|
|
Works everywhere — SSH, serial, pipes, ancient terminals.
|
|
|
|
Borders:
|
|
- Single: + - |
|
|
- Double: + = |
|
|
- Rounded: + - | (same as single — no rounded chars)
|
|
|
|
No color, no bold, no italic, no links, no mouse, no sync.
|
|
|
|
*** Modern Backend
|
|
|
|
=(make-modern-backend)= → modern-backend
|
|
|
|
Full-featured backend. Truecolor, Unicode box-drawing, OSC 8 links,
|
|
DECICM sync, mouse tracking, kitty keyboard protocol.
|
|
|
|
Borders:
|
|
- Single: ┌ ─ ┐ │ └ ┘
|
|
- Double: ╔ ═ ╗ ║ ╚ ╝
|
|
- Rounded: ╭ ─ ╮ │ ╰ ╯
|
|
|
|
** Test Suite
|
|
|
|
#+BEGIN_SRC lisp
|
|
(defpackage :cl-tty-backend-test
|
|
(:use :cl :fiveam)
|
|
(:export #:run!))
|
|
(in-package :cl-tty-backend-test)
|
|
|
|
(def-suite backend-suite :description "Backend protocol tests")
|
|
(in-suite backend-suite)
|
|
|
|
;; ── Simple Backend ──────────────────────────────────────────────
|
|
|
|
(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 (capable-p b :truecolor) nil "simple backend has no truecolor")
|
|
(shutdown-backend b)))
|
|
|
|
(test simple-backend-draw-text
|
|
"simple-backend renders text at position, ignoring style"
|
|
(let ((b (make-simple-backend)))
|
|
(initialize-backend b)
|
|
(draw-text b 0 0 "hello" nil nil)
|
|
;; No crash = pass (simple backend writes to *standard-output*)
|
|
(shutdown-backend b)
|
|
(is-t t)))
|
|
|
|
(test simple-backend-border-single
|
|
"simple-backend draws ASCII single border"
|
|
(let ((b (make-simple-backend)))
|
|
(initialize-backend b)
|
|
(draw-border b 0 0 10 5 :style :single)
|
|
(shutdown-backend b)
|
|
(is-t t)))
|
|
|
|
(test simple-backend-border-rounded
|
|
"simple-backend falls back to straight edges for rounded"
|
|
(let ((b (make-simple-backend)))
|
|
(initialize-backend b)
|
|
(draw-border b 0 0 10 5 :style :rounded)
|
|
;; No error — rounded falls back to single on simple
|
|
(shutdown-backend b)
|
|
(is-t t)))
|
|
|
|
;; ── Backend Capabilities ───────────────────────────────────────
|
|
|
|
(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 (capable-p b f) nil
|
|
(format nil "~s should not be supported on simple-backend" f)))
|
|
(shutdown-backend b)))
|
|
|
|
;; ── Backend Size ───────────────────────────────────────────────
|
|
|
|
(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)))
|
|
|
|
;; ── Drawing Primitives ─────────────────────────────────────────
|
|
|
|
(test draw-rect-fills-area
|
|
"draw-rect fills a rectangular area with background"
|
|
(let ((b (make-simple-backend)))
|
|
(initialize-backend b)
|
|
(draw-rect b 0 0 5 3 :bg nil)
|
|
(shutdown-backend b)
|
|
(is-t t)))
|
|
|
|
(test draw-text-multi-line
|
|
"draw-text handles strings with newlines"
|
|
(let ((b (make-simple-backend)))
|
|
(initialize-backend b)
|
|
(draw-text b 0 0 "line1~%line2" nil nil)
|
|
(shutdown-backend b)
|
|
(is-t t)))
|
|
|
|
;; ── Synchronization ────────────────────────────────────────────
|
|
|
|
(test sync-is-noop-on-simple
|
|
"begin-sync and end-sync are no-ops on simple-backend"
|
|
(let ((b (make-simple-backend)))
|
|
(initialize-backend b)
|
|
(begin-sync b)
|
|
(draw-text b 0 0 "in sync" nil nil)
|
|
(end-sync b)
|
|
(shutdown-backend b)
|
|
(is-t t)))
|
|
#+END_SRC
|
|
|
|
** Implementation
|
|
|
|
*** Package
|
|
|
|
#+BEGIN_SRC lisp
|
|
(defpackage :cl-tty.backend
|
|
(:use :cl)
|
|
(:export
|
|
;; Backend classes
|
|
#:backend #:simple-backend
|
|
;; Lifecycle
|
|
#:initialize-backend #:shutdown-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))
|
|
(in-package :cl-tty.backend)
|
|
#+END_SRC
|
|
|
|
*** Backend Base Class
|
|
|
|
#+BEGIN_SRC lisp
|
|
(defclass backend () ())
|
|
|
|
(defgeneric initialize-backend (backend)
|
|
(:method ((b backend)) b))
|
|
|
|
(defgeneric shutdown-backend (backend)
|
|
(:method ((b backend)) (values)))
|
|
|
|
(defgeneric backend-size (backend)
|
|
(:method ((b backend))
|
|
(values 80 24)))
|
|
|
|
(defgeneric backend-write (backend string))
|
|
|
|
(defgeneric backend-clear (backend)
|
|
(:method ((b backend))
|
|
(backend-write b (string #\escape) "[2J")
|
|
(cursor-move b 0 0)))
|
|
|
|
(defgeneric draw-text (backend x y string fg bg &key
|
|
bold italic underline reverse dim blink))
|
|
|
|
(defgeneric draw-border (backend x y width height
|
|
&key style fg bg title title-align))
|
|
|
|
(defgeneric draw-rect (backend x y width height &key bg))
|
|
|
|
(defgeneric draw-link (backend x y string url &key fg bg))
|
|
|
|
(defgeneric draw-ellipsis (backend x y width &key fg bg))
|
|
|
|
(defgeneric cursor-move (backend x y))
|
|
|
|
(defgeneric cursor-hide (backend)
|
|
(:method ((b backend)) (values)))
|
|
|
|
(defgeneric cursor-show (backend)
|
|
(:method ((b backend)) (values)))
|
|
|
|
(defgeneric cursor-style (backend shape &key blink)
|
|
(:method ((b backend) shape &key blink) (values)))
|
|
|
|
(defgeneric begin-sync (backend)
|
|
(:method ((b backend)) (values)))
|
|
|
|
(defgeneric end-sync (backend)
|
|
(:method ((b backend)) (values)))
|
|
|
|
(defgeneric read-event (backend &key timeout)
|
|
(:method ((b backend) &key timeout) (values nil nil)))
|
|
|
|
(defgeneric enable-mouse (backend)
|
|
(:method ((b backend)) (values)))
|
|
|
|
(defgeneric enable-bracketed-paste (backend)
|
|
(:method ((b backend)) (values)))
|
|
|
|
(defgeneric capable-p (backend feature)
|
|
(:method ((b backend) feature)
|
|
(declare (ignore feature))
|
|
nil))
|
|
#+END_SRC
|
|
|
|
*** Simple Backend
|
|
|
|
#+BEGIN_SRC lisp
|
|
(defclass simple-backend (backend)
|
|
((output-stream :initform *standard-output*
|
|
:accessor backend-output-stream)))
|
|
|
|
(defmethod initialize-backend ((b simple-backend))
|
|
b)
|
|
|
|
(defmethod shutdown-backend ((b simple-backend))
|
|
(values))
|
|
|
|
(defmethod backend-size ((b simple-backend))
|
|
;; Try ioctl, fall back to 80x24
|
|
(values 80 24))
|
|
|
|
(defmethod backend-write ((b simple-backend) string)
|
|
(let ((stream (backend-output-stream b)))
|
|
(write-string string stream)
|
|
(finish-output stream)
|
|
(length string)))
|
|
|
|
(defmethod draw-text ((b simple-backend) x y string fg bg
|
|
&key bold italic underline reverse dim blink)
|
|
(declare (ignore x y fg bg bold italic underline reverse dim blink))
|
|
(backend-write b string))
|
|
|
|
(defun %simple-border-char (edge-style pos)
|
|
"Return ASCII border character for EDGE-STYLE 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 #\|)))
|
|
|
|
(defmethod draw-border ((b simple-backend) x y width height
|
|
&key style fg bg title title-align)
|
|
(declare (ignore style fg bg title title-align))
|
|
(let ((h (%simple-border-char nil :horizontal))
|
|
(v (%simple-border-char nil :vertical)))
|
|
;; Top edge
|
|
(backend-write b (format nil "~%~v@{~a~:*~}" width h))
|
|
;; Sides
|
|
(loop for i from 1 below (1- height)
|
|
do (backend-write b (format nil "~%|~v@{~a~:*~}|" (- width 2) #\space)))
|
|
;; Bottom edge
|
|
(backend-write b (format nil "~%~v@{~a~:*~}" width h))))
|
|
|
|
(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))
|
|
|
|
(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))
|
|
|
|
(defmethod draw-ellipsis ((b simple-backend) x y width
|
|
&key fg bg)
|
|
(declare (ignore x y width fg bg))
|
|
(backend-write b "..."))
|
|
#+END_SRC
|