#+TITLE: Modern Backend #+STARTUP: content #+FILETAGS: :cl-tty:backend: * Overview The modern backend provides full-featured terminal rendering using raw escape sequences. It supports truecolor 24-bit color, OSC 8 hyperlinks, DECICM synchronized updates, SGR mouse tracking, kitty keyboard protocol, and Unicode box-drawing characters (single, double, rounded). All rendering functions produce CSI/OSC escape sequences directly --- no ncurses, no terminfo, no FFI. Color resolution handles named colors (~:red~, ~:blue~, etc.), hex strings (~"#FFD700"~), and semantic theme roles (~:accent~, ~:error~) via the ~*theme-colors*~ hash table. * Contract ** Color and attribute helpers - ~(hex-to-rgb hex)~ (r g b) --- parse "#RRGGBB" or "#RGB" - ~(sgr-fg color)~ escape string --- foreground color escape - ~(sgr-bg color)~ escape string --- background color escape - ~(sgr-attr attr)~ escape string --- attribute escape (bold, italic, etc.) ** Cursor helpers - ~(cursor-move-escape x y)~ escape string --- CSI cursor position - ~(cursor-style-escape shape blink)~ escape string --- DECSTR cursor shape ** Sync and link helpers - ~(decicm-begin)~ escape string --- enable synchronized updates - ~(decicm-end)~ escape string --- disable synchronized updates - ~(osc8-link url text)~ escape string --- OSC 8 hyperlink wrapper ** Border helpers - ~(border-char style pos)~ string --- Unicode box-drawing character ** Modern backend class - ~(make-modern-backend &key output-stream)~ modern-backend - Implements all ~backend~ protocol methods with escape sequences * Tests The test suite lives in =modern-tests.lisp= and uses FiveAM. Each test covers one logical behavior. ** Package and setup The test package uses =cl-tty.backend= to access internal symbols for white-box testing of escape generation. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (defpackage :cl-tty-modern-backend-test (:use :cl :fiveam :cl-tty.backend) (:export #:run-tests)) (in-package :cl-tty-modern-backend-test) #+END_SRC ** Suite definition A single suite groups all modern backend tests. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (def-suite modern-backend-suite :description "Modern backend tests") (in-suite modern-backend-suite) #+END_SRC ** Test runner The =run-tests= entry point is called by the CI test harness. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (defun run-tests () (let ((result (run 'modern-backend-suite))) (fiveam:explain! result) (uiop:quit 0))) #+END_SRC ** Constructor test Verifies that =make-modern-backend= returns an instance of the correct class. This is the most basic smoke test for the backend factory. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test make-modern-backend-creates "make-modern-backend returns a modern-backend instance" (let ((b (make-modern-backend))) (is (typep b 'cl-tty.backend::modern-backend)))) #+END_SRC ** SGR truecolor foreground escape Ensures a 6-digit hex string produces the correct 24-bit foreground escape sequence with red, green, and blue components in the right order. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test sgr-truecolor-foreground "SGR truecolor foreground escape is correct" (is (equal (cl-tty.backend::sgr-fg "#FFD700") (format nil "~C[38;2;255;215;0m" #\Esc)))) #+END_SRC ** SGR truecolor background escape Same as foreground but uses the =48= background prefix instead of =38=. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test sgr-truecolor-background "SGR truecolor background escape is correct" (is (equal (cl-tty.backend::sgr-bg "#1a1b26") (format nil "~C[48;2;26;27;38m" #\Esc)))) #+END_SRC ** SGR named color resolution Verifies that keyword symbols like =:red= and =:blue= resolve to the standard 8-color SGR codes (=31= foreground, =44= background). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test sgr-named-colors "SGR named colors resolve to 8-color codes" (is (equal (cl-tty.backend::sgr-fg :red) (format nil "~C[31m" #\Esc))) (is (equal (cl-tty.backend::sgr-bg :blue) (format nil "~C[44m" #\Esc)))) #+END_SRC ** SGR attribute escapes Each attribute keyword (=:bold=, =:italic=, =:underline=, =:reset=) should map to the correct SGR number. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test sgr-bold-italic "SGR attribute escapes are correct" (is (equal (cl-tty.backend::sgr-attr :bold) (format nil "~C[1m" #\Esc))) (is (equal (cl-tty.backend::sgr-attr :italic) (format nil "~C[3m" #\Esc))) (is (equal (cl-tty.backend::sgr-attr :underline) (format nil "~C[4m" #\Esc))) (is (equal (cl-tty.backend::sgr-attr :reset) (format nil "~C[0m" #\Esc)))) #+END_SRC ** Cursor move escape Verifies that =cursor-move-escape= produces a CSI =H= sequence with 1-indexed row and column. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test cursor-move-escape "cursor-move generates correct CSI escape" (let ((b (make-modern-backend))) (is (equal (cl-tty.backend::cursor-move-escape 5 10) (format nil "~C[11;6H" #\Esc))))) #+END_SRC ** Cursor style block Verifies the DECSTR escape for a block cursor without blinking (code 2). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test cursor-style-block "cursor-style :block generate correct escape" (let ((b (make-modern-backend))) (is (equal (cl-tty.backend::cursor-style-escape :block nil) (format nil "~C[2 q" #\Esc))))) #+END_SRC ** Cursor style bar Verifies the DECSTR escape for a bar cursor without blinking (code 6). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test cursor-style-bar "cursor-style :bar generate correct escape" (let ((b (make-modern-backend))) (is (equal (cl-tty.backend::cursor-style-escape :bar nil) (format nil "~C[6 q" #\Esc))))) #+END_SRC ** Cursor style underline with blink Verifies that =:underline= with =blink=t= produces code 5 (underline blinking), which is base 4 + blink offset 1. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test cursor-style-underline-blink "cursor-style :underline with blink" (let ((b (make-modern-backend))) (is (equal (cl-tty.backend::cursor-style-escape :underline t) (format nil "~C[5 q" #\Esc))))) #+END_SRC ** DECICM synchronized update escapes Confirms that =decicm-begin= and =decicm-end= produce =?2026h= and =?2026l= respectively. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test decicm-escapes "DECICM synchronized update escapes" (is (equal (cl-tty.backend::decicm-begin) (format nil "~C[?2026h" #\Esc))) (is (equal (cl-tty.backend::decicm-end) (format nil "~C[?2026l" #\Esc)))) #+END_SRC ** OSC 8 hyperlink escape Verifies the full OSC 8 wrapping: opening sequence with URL, the text, and the closing sequence. The FORMAT string uses ~~ for literal tilde and ~\\ for literal backslash. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test osc8-escape "OSC 8 hyperlink escape wraps text" (is (equal (cl-tty.backend::osc8-link "http://example.com" "click here") (format nil "~C]8;;http://example.com~C\\click here~C]8;;~C\\" #\Esc #\Esc #\Esc #\Esc)))) #+END_SRC ** Hex color parsing (gold) Verifies that ="#FFD700"= parses to (255, 215, 0). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test hex-color-parsing "hex-to-rgb parses valid hex colors" (multiple-value-bind (r g b) (cl-tty.backend::hex-to-rgb "#FFD700") (is (= r 255)) (is (= g 215)) (is (= b 0)))) #+END_SRC ** Hex color parsing (black) Verifies all-zero parsing. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test hex-color-black "hex-to-rgb parses black" (multiple-value-bind (r g b) (cl-tty.backend::hex-to-rgb "#000000") (is (= r 0)) (is (= g 0)) (is (= b 0)))) #+END_SRC ** Hex color parsing (3-digit short form) Verifies that ="#F00"= expands to ="#FF0000"= = (255, 0, 0). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test hex-color-short-form "hex-to-rgb parses 3-digit hex" (multiple-value-bind (r g b) (cl-tty.backend::hex-to-rgb "#F00") (is (= r 255)) (is (= g 0)) (is (= b 0)))) #+END_SRC ** Border characters --- rounded style Confirms that =:rounded= style maps to the Unicode box-drawing characters for the four corners and edges. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test border-char-rounded "modern-border-char returns Unicode box-drawing for rounded style" (is (equal (cl-tty.backend::border-char :rounded :top-left) "╭")) (is (equal (cl-tty.backend::border-char :rounded :horizontal) "─")) (is (equal (cl-tty.backend::border-char :rounded :vertical) "│")) (is (equal (cl-tty.backend::border-char :rounded :bottom-right) "╯"))) #+END_SRC ** Border characters --- double style Confirms that =:double= style maps to double-line box-drawing characters. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test border-char-double "modern-border-char returns double-line chars" (is (equal (cl-tty.backend::border-char :double :top-left) "╔")) (is (equal (cl-tty.backend::border-char :double :horizontal) "═")) (is (equal (cl-tty.backend::border-char :double :vertical) "║"))) #+END_SRC ** Suspend/resume backend Verifies that suspend-backend and resume-backend are no-ops when called on a backend not attached to a real terminal (no errors, return nil). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern-tests.lisp (test suspend-resume-noop "suspend-backend and resume-backend are no-ops in test context" (let ((b (make-modern-backend))) (is (null (multiple-value-list (suspend-backend b)))) (is (null (multiple-value-list (resume-backend b)))))) #+END_SRC * Implementation ** Color and attribute helpers *** hex-to-rgb ~hex-to-rgb~ parses hex color strings into (r g b) triplets. Handles both 6-digit (fully specified) and 3-digit (shorthand) formats. The 3-digit form expands each hexit by duplicating it (=#F00= => =#FF0000=). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (in-package :cl-tty.backend) (defun hex-to-rgb (hex) "Parse a hex color string like \"#FFD700\" into (values r g b). Also handles 3-digit hex like \"#F00\" (expands to \"#FF0000\")." (let ((clean (string-trim '(#\# #\Space) hex))) (if (= (length clean) 3) ;; Expand 3-digit: #F00 -> #FF0000 (let* ((r (parse-integer (subseq clean 0 1) :radix 16 :junk-allowed t)) (g (parse-integer (subseq clean 1 2) :radix 16 :junk-allowed t)) (b (parse-integer (subseq clean 2 3) :radix 16 :junk-allowed t))) (values (+ r (* r 16)) (+ g (* g 16)) (+ b (* b 16)))) (values (parse-integer (subseq clean 0 2) :radix 16 :junk-allowed t) (parse-integer (subseq clean 2 4) :radix 16 :junk-allowed t) (parse-integer (subseq clean 4 6) :radix 16 :junk-allowed t))))) #+END_SRC *** *named-colors* Maps keyword color names to 8-color SGR index values. Used as the primary lookup in =sgr-fg= and =sgr-bg= before falling back to the theme colors hash table. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defparameter *named-colors* '((:black . 0) (:red . 1) (:green . 2) (:yellow . 3) (:blue . 4) (:magenta . 5) (:cyan . 6) (:white . 7))) #+END_SRC *** *theme-colors* Hash table mapping semantic theme role keywords to hex color strings. Populated by the theme system's =load-preset=. When a keyword is not in =*named-colors*=, =sgr-fg= and =sgr-bg= consult this table as a fallback, enabling user themes to define custom color roles. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defvar *theme-colors* (make-hash-table :test 'eq) "Hash table mapping theme keywords to hex color strings. Populated by the theme system's load-preset. Checked by sgr-fg/sgr-bg as a fallback when a keyword is not in *named-colors*.") #+END_SRC *** sgr-fg ~sgr-fg~ produces the SGR foreground escape sequence. Resolution chain: hex string => named color => semantic theme role => empty string if unresolved. Truecolor uses =38;2;R;G;B=, named colors use =3n=. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun sgr-fg (color) "Return SGR foreground escape for COLOR." (if (null color) "" (cond ((and (stringp color) (char= (char color 0) #\#)) (multiple-value-bind (r g b) (hex-to-rgb color) (format nil "~C[38;2;~d;~d;~dm" #\Esc r g b))) ((keywordp color) (let ((index (cdr (assoc color *named-colors*)))) (if index (format nil "~C[~dm" #\Esc (+ 30 index)) (let ((hex (gethash color *theme-colors*))) (if hex (multiple-value-bind (r g b) (hex-to-rgb hex) (format nil "~C[38;2;~d;~d;~dm" #\Esc r g b)) ""))))) (t "")))) #+END_SRC *** sgr-bg ~sgr-bg~ produces the SGR background escape. Same resolution chain as =sgr-fg= but uses =48;2;R;G;B= for truecolor and =4n= for named colors. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun sgr-bg (color) "Return SGR background escape for COLOR." (if (null color) "" (cond ((and (stringp color) (char= (char color 0) #\#)) (multiple-value-bind (r g b) (hex-to-rgb color) (format nil "~C[48;2;~d;~d;~dm" #\Esc r g b))) ((keywordp color) (let ((index (cdr (assoc color *named-colors*)))) (if index (format nil "~C[~dm" #\Esc (+ 40 index)) (let ((hex (gethash color *theme-colors*))) (if hex (multiple-value-bind (r g b) (hex-to-rgb hex) (format nil "~C[48;2;~d;~d;~dm" #\Esc r g b)) ""))))) (t "")))) #+END_SRC *** *sgr-attr-codes* Maps attribute keywords to SGR parameter numbers. Covers bold, dim, italic, underline, blink, reverse video, and reset. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defparameter *sgr-attr-codes* '((:bold . 1) (:dim . 2) (:italic . 3) (:underline . 4) (:blink . 5) (:reverse . 7) (:reset . 0))) #+END_SRC *** sgr-attr ~sgr-attr~ looks up the keyword in =*sgr-attr-codes*= and produces the matching SGR escape. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun sgr-attr (attr) "Return SGR attribute escape for ATTR keyword." (let ((code (cdr (assoc attr *sgr-attr-codes*)))) (if code (format nil "~C[~dm" #\Esc code) ""))) #+END_SRC ** Cursor escapes *** cursor-move-escape Produces a CSI =H= (CUP) sequence to position the cursor. Coordinates are 1-indexed: =cursor-move-escape 0 0= moves to row 1, column 1. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun cursor-move-escape (x y) "Return CSI escape to move cursor to (x, y), 1-indexed." (format nil "~C[~d;~dH" #\Esc (1+ y) (1+ x))) #+END_SRC *** cursor-style-escape Produces a DECSTR sequence (=CSI Ps q=) to set the cursor shape. Base codes: block=2, underline=4, bar=6. When =blink= is true the code is incremented by 1 (e.g. blinking block = code 3). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun cursor-style-escape (shape blink) "Return DECSTR escape for cursor shape." (let* ((base (case shape (:block 2) (:underline 4) (:bar 6) (t 2))) (code (if blink (1+ base) base))) (format nil "~C[~d q" #\Esc code))) #+END_SRC ** Sync and link escapes *** decicm-begin Enables DEC private mode 2026 (synchronized updates). All output between =begin= and =end= is buffered by the terminal and rendered atomically. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun decicm-begin () "Return escape to enable synchronized updates." (format nil "~C[?2026h" #\Esc)) #+END_SRC *** decicm-end Disables DEC private mode 2026, flushing the buffered frame to the display. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun decicm-end () "Return escape to disable synchronized updates." (format nil "~C[?2026l" #\Esc)) #+END_SRC *** osc8-link Wraps text in an OSC 8 hyperlink. The opening sequence carries the URL, the closing sequence (=ESC]8;;ESC\)=) terminates the link. This allows clickable text in terminals that support the protocol. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun osc8-link (url text) "Wrap TEXT in an OSC 8 hyperlink to URL." (format nil "~C]8;;~A~C\\~A~C]8;;~C\\" #\Esc url #\Esc text #\Esc #\Esc)) #+END_SRC ** Border characters *** *border-chars* Lookup alist mapping =(style position)= pairs to Unicode box-drawing characters. Covers single, double, and rounded styles with all four corners plus horizontal and vertical connectors. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defparameter *border-chars* '(((:single :top-left) . "┌") ((:single :top-right) . "┐") ((:single :bottom-left) . "└") ((:single :bottom-right) . "┘") ((:single :horizontal) . "─") ((:single :vertical) . "│") ((:double :top-left) . "╔") ((:double :top-right) . "╗") ((:double :bottom-left) . "╚") ((:double :bottom-right) . "╝") ((:double :horizontal) . "═") ((:double :vertical) . "║") ((:rounded :top-left) . "╭") ((:rounded :top-right) . "╮") ((:rounded :bottom-left) . "╰") ((:rounded :bottom-right) . "╯") ((:rounded :horizontal) . "─") ((:rounded :vertical) . "│"))) #+END_SRC *** border-char Looks up a border character by style and position. Falls back to horizontal/vertical lines (=U+2500=, =U+2502=) if the style is unknown for edge positions, or =+= for corners --- ensuring the UI never shows a blank gap. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun border-char (style pos) "Return the Unicode box-drawing character for STYLE at POS." (let ((char (cdr (assoc (list style pos) *border-chars* :test #'equal)))) (or char (if (member pos '(:horizontal :vertical)) (case pos (:horizontal "─") (:vertical "│")) "+")))) #+END_SRC ** Modern backend class *** modern-backend (class) Subclasses the abstract =backend= class. =output-stream= is where escape sequences are written; =in-sync-p= tracks whether we are inside a DECICM synchronized update block. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defclass modern-backend (backend) ((output-stream :initform *standard-output* :initarg :output-stream :accessor backend-output-stream) (in-sync-p :initform nil :accessor in-sync-p))) #+END_SRC *** make-modern-backend Factory function that creates a =modern-backend= instance. Accepts an optional =output-stream=; defaults to =*standard-output*=. The =color-palette= argument is ignored in favor of the dynamic =*theme-colors*= hash table. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defun make-modern-backend (&key color-palette output-stream) (declare (ignore color-palette)) (make-instance 'modern-backend :output-stream (or output-stream *standard-output*))) #+END_SRC ** Lifecycle *** initialize-backend Enters the alternate screen buffer, enables mouse tracking (basic + drag + SGR), bracketed paste mode, and the Kitty keyboard protocol. Hides the cursor and flushes the stream. Returns the backend instance for chaining. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod initialize-backend ((b modern-backend)) (backend-write b (format nil "~C[?1049h" #\Esc)) ; alt screen (backend-write b (format nil "~C[?1000h" #\Esc)) ; mouse basic (backend-write b (format nil "~C[?1002h" #\Esc)) ; mouse drag (backend-write b (format nil "~C[?1006h" #\Esc)) ; SGR mouse (backend-write b (format nil "~C[?2004h" #\Esc)) ; bracketed paste (backend-write b (format nil "~C[?u" #\Esc)) ; kitty keyboard (cursor-hide b) (finish-output (backend-output-stream b)) b) #+END_SRC *** shutdown-backend Restores the terminal: shows the cursor, disables the Kitty keyboard protocol, bracketed paste, SGR mouse, drag, basic mouse, and finally leaves the alternate screen. Returns =nil= (via =(values)=). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod shutdown-backend ((b modern-backend)) (cursor-show b) (backend-write b (format nil "~C[?u" #\Esc)) (backend-write b (format nil "~C[?2004l" #\Esc)) (backend-write b (format nil "~C[?1006l" #\Esc)) (backend-write b (format nil "~C[?1002l" #\Esc)) (backend-write b (format nil "~C[?1000l" #\Esc)) (backend-write b (format nil "~C[?1049l" #\Esc)) ; normal screen (finish-output (backend-output-stream b)) (values)) #+END_SRC *** Suspend backend (temporary) Temporarily suspends the modern backend, restoring the terminal to a usable state so the shell (or parent process) can take over. Called before =SIGTSTP= or similar process suspension. Shows the cursor and exits the alternate screen buffer so the user sees the normal terminal content. Does NOT disable mouse modes or kitty keyboard — those would add ~100ms of overhead on every suspend/resume cycle and are harmless while suspended (the terminal just ignores the escape sequences). #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod suspend-backend ((b modern-backend)) (cursor-show b) (backend-write b (format nil "~C[?1049l" #\Esc)) ; normal screen (cursor-move b 0 0) (finish-output (backend-output-stream b)) (values)) #+END_SRC *** Resume backend (after suspend) Re-initializes the modern backend after a suspension. Called after =SIGCONT= or similar process resume. Re-enters the alternate screen buffer and re-enables all input features (mouse, bracketed paste, kitty keyboard). The application is responsible for redrawing the full screen after resume. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod resume-backend ((b modern-backend)) (backend-write b (format nil "~C[?1049h" #\Esc)) ; alt screen (backend-write b (format nil "~C[?1000h" #\Esc)) ; mouse basic (backend-write b (format nil "~C[?1002h" #\Esc)) ; mouse drag (backend-write b (format nil "~C[?1006h" #\Esc)) ; SGR mouse (backend-write b (format nil "~C[?2004h" #\Esc)) ; bracketed paste (backend-write b (format nil "~C[?u" #\Esc)) ; kitty keyboard (cursor-hide b) (finish-output (backend-output-stream b)) (values)) #+END_SRC ** Backend-size via ioctl *** backend-size Uses ioctl (TIOCGWINSZ = 21523) to query actual terminal dimensions from the kernel, with a ~/dev/tty~ fallback and 80x24 last resort. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod backend-size ((b modern-backend)) ;; Try ioctl on stdout, fall back to /dev/tty, then 80x24. ;; Each arm uses multiple-value-bind/values to preserve both cols and rows ;; (or discards secondary values, so we avoid it for multi-value returns). (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) ;; cols (sb-alien:deref winsize 0)))) ;; rows (sb-alien:free-alien winsize)))) (if (and cols rows (> cols 0) (> rows 0)) (values cols rows) ;; Direct ioctl on /dev/tty. (multiple-value-bind (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 (let ((cols (sb-alien:deref winsize 1)) (rows (sb-alien:deref winsize 0))) (values cols rows))))) (sb-unix:unix-close tty-fd))))) (if (and cols rows (> cols 0) (> rows 0)) (values cols rows) (values 80 24)))))) #+END_SRC ** Capability query and write *** backend-write Writes a string to the backend's output stream. Does NOT flush — the caller is responsible for calling ~finish-output~ at appropriate sync points (frame boundaries via ~end-sync~, initialization, shutdown). Returns the string length for protocol compatibility. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod backend-write ((b modern-backend) string) (let ((stream (backend-output-stream b))) (write-string string stream) (length string))) #+END_SRC *** capable-p Advertises which features this backend supports. =modern-backend= supports truecolor, OSC 8 hyperlinks, DECICM sync, SGR mouse, bracketed paste, cursor style control, and the Kitty keyboard protocol. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod capable-p ((b modern-backend) feature) (member feature '(:truecolor :osc8 :sync :mouse :bracketed-paste :cursor-style :kitty-keyboard))) #+END_SRC ** Drawing *** draw-text Combines cursor positioning, SGR colors, optional attributes, the text itself, and a reset into a single concatenated string. Minimizes output calls --- one =backend-write= per draw operation --- by packing everything into one buffer. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod draw-text ((b modern-backend) x y string fg bg &key bold italic underline reverse dim blink &allow-other-keys) (let ((parts (list (cursor-move-escape x y) (sgr-fg fg) (sgr-bg bg) (when bold (sgr-attr :bold)) (when italic (sgr-attr :italic)) (when underline (sgr-attr :underline)) (when reverse (sgr-attr :reverse)) (when dim (sgr-attr :dim)) (when blink (sgr-attr :blink)) string (sgr-attr :reset)))) (backend-write b (apply #'concatenate 'string parts)))) #+END_SRC *** draw-border Builds the full border as three distinct string parts (top with optional title, repeated mid sections, bottom) and writes them with minimal output calls. The title can be left-aligned or centered within the top border line. Uses the border character lookup for the chosen style. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod draw-border ((b modern-backend) x y width height &key style fg bg title title-align) (let* ((s (or style :single)) (tl (border-char s :top-left)) (tr (border-char s :top-right)) (bl (border-char s :bottom-left)) (br (border-char s :bottom-right)) (h (border-char s :horizontal)) (v (border-char s :vertical)) (fg-esc (sgr-fg fg)) (bg-esc (sgr-bg bg)) (reset (sgr-attr :reset)) (inner-width (- width 2)) (hc (char h 0)) (top (if (and title (plusp (length title))) (let* ((align (or title-align :left)) (max-tlen (- inner-width 2)) (tlen (min (length title) max-tlen)) (trunc-title (subseq title 0 tlen))) (ecase align (:left (let ((right-hyphens (- inner-width tlen 2))) (concatenate 'string fg-esc bg-esc tl (string #\Space) trunc-title (string #\Space) (make-string (max 0 right-hyphens) :initial-element hc) tr reset (string #\Newline)))) (:center (let* ((total-pad (- inner-width tlen)) (left-pad (floor total-pad 2)) (right-pad (- total-pad left-pad))) (concatenate 'string fg-esc bg-esc tl (make-string left-pad :initial-element hc) trunc-title (make-string right-pad :initial-element hc) tr reset (string #\Newline)))))) (concatenate 'string fg-esc bg-esc tl (make-string inner-width :initial-element hc) tr reset (string #\Newline)))) (mid (concatenate 'string fg-esc bg-esc v (make-string inner-width :initial-element #\Space) v reset (string #\Newline))) (bot (concatenate 'string fg-esc bg-esc bl (make-string inner-width :initial-element hc) br reset))) (backend-write b top) (loop repeat (- height 2) do (backend-write b mid)) (backend-write b bot))) #+END_SRC *** draw-rect Fills a rectangular area with a background color. For each row, moves the cursor and writes a filled line. This is simpler than =draw-border= because it has no border characters --- just spaces with a background color. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod draw-rect ((b modern-backend) x y width height &key bg) (let* ((bg-esc (sgr-bg bg)) (reset (sgr-attr :reset)) (line (concatenate 'string bg-esc (make-string width :initial-element #\Space) reset ""))) (loop :for row :from 0 :below height :do (backend-write b (cursor-move-escape x (+ y row))) (backend-write b line)))) #+END_SRC *** draw-link Draws a hyperlinked text at position (x, y). Combines cursor positioning, optional fg/bg colors, the OSC 8 link wrapper around the text, and a reset. This lets the user click the text to open the URL in terminals that support OSC 8. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod draw-link ((b modern-backend) x y string url &key fg bg) (let ((parts (list (cursor-move-escape x y) (sgr-fg fg) (sgr-bg bg) (osc8-link url string) (sgr-attr :reset)))) (backend-write b (apply #'concatenate 'string parts)))) #+END_SRC *** draw-ellipsis Draws a three-dot ellipsis at the given position. The =width= parameter is ignored since dots have a fixed visual length; delegates to =draw-text= for uniform rendering. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod draw-ellipsis ((b modern-backend) x y width &key fg bg) (declare (ignore width)) (let ((dots "...")) (draw-text b x y dots fg bg))) #+END_SRC ** Cursor and input methods *** cursor-move Delegates to =cursor-move-escape= and writes the resulting CSI sequence to the output stream. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod cursor-move ((b modern-backend) x y) (backend-write b (cursor-move-escape x y))) #+END_SRC *** cursor-hide Sends the DECTCEM private mode =?25l= to hide the cursor. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod cursor-hide ((b modern-backend)) (backend-write b (format nil "~C[?25l" #\Esc))) #+END_SRC *** cursor-show Sends =?25h= to restore the cursor visibility. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod cursor-show ((b modern-backend)) (backend-write b (format nil "~C[?25h" #\Esc))) #+END_SRC *** cursor-style Sets the cursor shape (block/underline/bar, optionally blinking) by delegating to =cursor-style-escape=. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod cursor-style ((b modern-backend) shape &key blink) (backend-write b (cursor-style-escape shape blink))) #+END_SRC *** enable-mouse Enables basic mouse tracking, button-event tracking (drag), and SGR extended mouse mode. These three modes together give full mouse support while staying compatible with modern terminal emulators. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod enable-mouse ((b modern-backend)) (backend-write b (format nil "~C[?1000h" #\Esc)) (backend-write b (format nil "~C[?1002h" #\Esc)) (backend-write b (format nil "~C[?1006h" #\Esc)) (finish-output (backend-output-stream b))) #+END_SRC *** enable-bracketed-paste Enables bracketed paste mode, where the terminal wraps pasted text in =ESC[200~= and =ESC[201~= delimiters. This allows the application to distinguish user input from pasted content. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod enable-bracketed-paste ((b modern-backend)) (backend-write b (format nil "~C[?2004h" #\Esc)) (finish-output (backend-output-stream b))) #+END_SRC *** begin-sync Begins a synchronized update frame using DECICM. Sets the =in-sync-p= slot so other methods can check whether we are inside a sync block. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod begin-sync ((b modern-backend)) (setf (in-sync-p b) t) (backend-write b (decicm-begin))) #+END_SRC *** end-sync Ends the synchronized update frame and flushes the output, causing the terminal to render the buffered changes atomically. #+BEGIN_SRC lisp :tangle ~/.local/share/cl-tty/src/backend/modern.lisp (defmethod end-sync ((b modern-backend)) (setf (in-sync-p b) nil) (backend-write b (decicm-end)) (finish-output (backend-output-stream b))) #+END_SRC