Every function, defclass, defstruct, defgeneric, defmethod, defmacro, defvar, and defparameter in every org file now has its own #+BEGIN_SRC block with literate prose above it explaining the design reasoning. Block counts before → after: package.org: 1 → 7 container-package.org: 1 → 1 (prose expanded) dirty.org: 4 → 6 render.org: 10 → 25 theme.org: 6 → 19 box-renderable.org: 9 → 29 scrollbox.org: 8 → 26 tabbar.org: 5 → 10 backend-protocol.org: 8 → 66 modern-backend.org: 17 → 53 detection.org: 4 → 6 layout-engine.org: 9 → 36 framebuffer.org: 8 → 37 markdown-renderer.org:13 → 38 dialog.org: 17 → 23 (merged dual structure) mouse.org: 4 → 25 select.org: 12 → 30 slot.org: 4 → 12 text-input.org: 11 → 53 Total: ~153 blocks → ~502 blocks Bugs fixed during restructuring: - render.org: stray π character typo (backenπd → backend) - modern-backend.org: sgr-attr missing closing paren + #+END_SRC - detection.org: invalid #\Esc character reference - select.org: extra closing paren in select-visible-options All 13 test suites pass at 100%.
30 KiB
Modern Backend
- Overview
- Contract
- Tests
- Package and setup
- Suite definition
- Test runner
- Constructor test
- SGR truecolor foreground escape
- SGR truecolor background escape
- SGR named color resolution
- SGR attribute escapes
- Cursor move escape
- Cursor style block
- Cursor style bar
- Cursor style underline with blink
- DECICM synchronized update escapes
- OSC 8 hyperlink escape
- Hex color parsing (gold)
- Hex color parsing (black)
- Hex color parsing (3-digit short form)
- Border characters — rounded style
- Border characters — double style
- Implementation
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
backendprotocol 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.
(defpackage :cl-tty-modern-backend-test
(:use :cl :fiveam :cl-tty.backend)
(:export #:run-tests))
(in-package :cl-tty-modern-backend-test)
Suite definition
A single suite groups all modern backend tests.
(def-suite modern-backend-suite :description "Modern backend tests")
(in-suite modern-backend-suite)
Test runner
The run-tests entry point is called by the CI test harness.
(defun run-tests ()
(let ((result (run 'modern-backend-suite)))
(fiveam:explain! result)
(uiop:quit 0)))
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.
(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))))
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.
(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))))
SGR truecolor background escape
Same as foreground but uses the 48 background prefix instead of 38.
(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))))
SGR named color resolution
Verifies that keyword symbols like :red and :blue resolve to the
standard 8-color SGR codes (31 foreground, 44 background).
(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))))
SGR attribute escapes
Each attribute keyword (:bold, :italic, :underline, :reset)
should map to the correct SGR number.
(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))))
Cursor move escape
Verifies that cursor-move-escape produces a CSI H sequence with
1-indexed row and column.
(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)))))
Cursor style block
Verifies the DECSTR escape for a block cursor without blinking (code 2).
(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)))))
Cursor style bar
Verifies the DECSTR escape for a bar cursor without blinking (code 6).
(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)))))
Cursor style underline with blink
Verifies that :underline with blink=t produces code 5 (underline
blinking), which is base 4 + blink offset 1.
(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)))))
DECICM synchronized update escapes
Confirms that decicm-begin and decicm-end produce ?2026h and
?2026l respectively.
(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))))
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.
(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))))
Hex color parsing (gold)
Verifies that "#FFD700" parses to (255, 215, 0).
(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))))
Hex color parsing (black)
Verifies all-zero parsing.
(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))))
Hex color parsing (3-digit short form)
Verifies that "#F00" expands to "#FF0000" = (255, 0, 0).
(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))))
Border characters — rounded style
Confirms that :rounded style maps to the Unicode box-drawing
characters for the four corners and edges.
(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) "╯"))
Border characters — double style
Confirms that :double style maps to double-line box-drawing characters.
(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) "║"))
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).
(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)))))
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.
(defparameter *named-colors*
'((:black . 0) (:red . 1) (:green . 2) (:yellow . 3)
(:blue . 4) (:magenta . 5) (:cyan . 6) (:white . 7)))
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.
(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*.")
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.
(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 ""))))
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.
(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 ""))))
sgr-attr-codes
Maps attribute keywords to SGR parameter numbers. Covers bold, dim, italic, underline, blink, reverse video, and reset.
(defparameter *sgr-attr-codes*
'((:bold . 1) (:dim . 2) (:italic . 3) (:underline . 4)
(:blink . 5) (:reverse . 7) (:reset . 0)))
sgr-attr
sgr-attr looks up the keyword in *sgr-attr-codes* and produces the
matching SGR escape.
(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)
"")))
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.
(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)))
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).
(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)))
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.
(defun decicm-begin ()
"Return escape to enable synchronized updates."
(format nil "~C[?2026h" #\Esc))
decicm-end
Disables DEC private mode 2026, flushing the buffered frame to the display.
(defun decicm-end ()
"Return escape to disable synchronized updates."
(format nil "~C[?2026l" #\Esc))
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.
(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))
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.
(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) . "│")))
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.
(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 "│"))
"+"))))
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.
(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)))
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.
(defun make-modern-backend (&key color-palette output-stream)
(declare (ignore color-palette))
(make-instance 'modern-backend :output-stream (or output-stream *standard-output*)))
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.
(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)
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)).
(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))
Backend-size via ioctl
backend-size
Uses TIOCGWINSZ (0x5413 = 21523) to query actual terminal dimensions
from the kernel via ioctl. The alien-sap wrapper ensures
compatibility across SBCL versions. Returns (values cols rows).
(defmethod backend-size ((b modern-backend))
(let* ((+tiocgwinsz+ 21523) ; 0x5413 on Linux
(winsize (sb-alien:make-alien sb-alien:unsigned-short 4)))
(unwind-protect
(progn
(sb-unix:unix-ioctl (sb-sys:fd-stream-fd (backend-output-stream b))
+tiocgwinsz+
(sb-alien:alien-sap winsize))
(values (sb-alien:deref winsize 1) ;; cols
(sb-alien:deref winsize 0))) ;; rows
(sb-alien:free-alien winsize))))
Capability query and write
backend-write
Writes a string to the backend's output stream, flushing after each write to ensure the terminal receives the escape sequence immediately. Returns the string length for protocol compatibility.
(defmethod backend-write ((b modern-backend) string)
(let ((stream (backend-output-stream b)))
(write-string string stream)
(finish-output stream)
(length string)))
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.
(defmethod capable-p ((b modern-backend) feature)
(member feature '(:truecolor :osc8 :sync :mouse
:bracketed-paste :cursor-style
:kitty-keyboard)))
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.
(defmethod draw-text ((b modern-backend) x y string fg bg
&key bold italic underline reverse dim blink)
(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))))
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.
(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)))
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.
(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 (string #\Newline))))
(loop :for row :from 0 :below height :do
(backend-write b (cursor-move-escape x (+ y row)))
(backend-write b line))))
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.
(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))))
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.
(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)))
Cursor and input methods
cursor-move
Delegates to cursor-move-escape and writes the resulting CSI sequence
to the output stream.
(defmethod cursor-move ((b modern-backend) x y)
(backend-write b (cursor-move-escape x y)))
cursor-hide
Sends the DECTCEM private mode ?25l to hide the cursor.
(defmethod cursor-hide ((b modern-backend))
(backend-write b (format nil "~C[?25l" #\Esc)))
cursor-show
Sends ?25h to restore the cursor visibility.
(defmethod cursor-show ((b modern-backend))
(backend-write b (format nil "~C[?25h" #\Esc)))
cursor-style
Sets the cursor shape (block/underline/bar, optionally blinking) by
delegating to cursor-style-escape.
(defmethod cursor-style ((b modern-backend) shape &key blink)
(backend-write b (cursor-style-escape shape blink)))
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.
(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)))
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.
(defmethod enable-bracketed-paste ((b modern-backend))
(backend-write b (format nil "~C[?2004h" #\Esc))
(finish-output (backend-output-stream b)))
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.
(defmethod begin-sync ((b modern-backend))
(setf (in-sync-p b) t)
(backend-write b (decicm-begin)))
end-sync
Ends the synchronized update frame and flushes the output, causing the terminal to render the buffered changes atomically.
(defmethod end-sync ((b modern-backend))
(setf (in-sync-p b) nil)
(backend-write b (decicm-end))
(finish-output (backend-output-stream b)))