From e8b37f62689c9c2906beb9b52d7fbbdd765d34e7 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Thu, 14 May 2026 08:55:56 -0400 Subject: [PATCH] fix: add CSI positioning and ioctl sizing to simple-backend - backend-size now uses TIOCGWINSZ ioctl (like modern-backend) - draw-text adds \033[row;colH CSI cursor positioning - draw-rect fills background with space characters at position - draw-border uses CSI positioning instead of raw newlines+spaces - Added cursor-hide/cursor-show, cursor-move, initialize/shutdown - Detection: broader DA1 check (any ANSI response, not just kitty) - Detection: added TERM-based fallback for modern terminal detection --- src/backend/detection.lisp | 26 +++++++++++---- src/backend/simple.lisp | 68 ++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 28 deletions(-) diff --git a/src/backend/detection.lisp b/src/backend/detection.lisp index 9ca8ba5..ca4b932 100644 --- a/src/backend/detection.lisp +++ b/src/backend/detection.lisp @@ -31,13 +31,26 @@ TIMEOUT seconds. Returns the response string, or nil if no response." response))) (defun detect-backend-by-da1 () - "Send DA1 (ESC[c) query and check for kitty terminal response code. -Returns T if terminal reports kitty compatibility codes." + "Send DA1 (ESC[c) query and check for any terminal response. +Returns T if the terminal responds to DA1 (indicating an ANSI-compatible terminal)." (let ((response (query-terminal (format nil "~C[c" (code-char 27))))) (when response - ;; DA1 response format: ESC [ ? digits ; digits c - ;; Kitty reports code 62 in the response - (search "?62" response)))) + ;; Any DA1 response (ESC [ ? digits ... c) means the terminal + ;; understands ANSI escape sequences — good enough for modern-backend + (> (length response) 0)))) + +(defun detect-backend-by-term () + "Check TERM environment variable for modern terminal types. +Returns :modern if TERM contains xterm, tmux, screen, kitty, +alacritty, foot, wezterm, or ghostty." + (let ((term (sb-ext:posix-getenv "TERM"))) + (when term + ;; Known non-modern terms + (unless (or (string-equal term "dumb") + (string-equal term "dump") + (string-equal term "emacs") + (search "52" term)) ; VT52 — no ANSI + :modern)))) (defun detect-backend () "Auto-detect the appropriate backend for the current terminal. @@ -47,6 +60,7 @@ Result is cached in *detected-backend* for subsequent calls." (setf *detected-backend* (if (and (detect-backend-by-tty) (or (eql (detect-backend-by-env) :modern) - (detect-backend-by-da1))) + (detect-backend-by-da1) + (eql (detect-backend-by-term) :modern))) (make-modern-backend) (make-simple-backend))))) diff --git a/src/backend/simple.lisp b/src/backend/simple.lisp index 9833cea..26e5f4a 100644 --- a/src/backend/simple.lisp +++ b/src/backend/simple.lisp @@ -10,20 +10,36 @@ :output-stream (or output-stream *standard-output*))) (defmethod initialize-backend ((b simple-backend)) + (backend-write b (format nil "~C[2J~C[H" #\Esc #\Esc)) ; clear + home + (backend-write b (format nil "~C[?25l" #\Esc)) ; hide cursor b) (defmethod shutdown-backend ((b simple-backend)) + (backend-write b (format nil "~C[?25h" #\Esc)) ; show cursor (values)) (defmethod suspend-backend ((b simple-backend)) + (backend-write b (format nil "~C[?25h" #\Esc)) (values)) (defmethod resume-backend ((b simple-backend)) + (backend-write b (format nil "~C[?25l" #\Esc)) (values)) (defmethod backend-size ((b simple-backend)) - ;; Try ioctl, fall back to 80x24 - (values 80 24)) + (or (ignore-errors + (let* ((+tiocgwinsz+ 21523) + (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) + (sb-alien:deref winsize 0))) + (sb-alien:free-alien winsize)))) + (values 80 24))) (defmethod backend-write ((b simple-backend) string) (let ((stream (backend-output-stream b))) @@ -31,16 +47,29 @@ (finish-output stream) (length string))) +(defmethod capable-p ((b simple-backend) feature) + (declare (ignore feature)) + nil) + +(defun %cursor-move (x y) + (format nil "~C[~d;~dH" #\Esc (1+ y) (1+ x))) + +(defmethod cursor-hide ((b simple-backend)) + (backend-write b (format nil "~C[?25l" #\Esc))) + +(defmethod cursor-show ((b simple-backend)) + (backend-write b (format nil "~C[?25h" #\Esc))) + +(defmethod cursor-move ((b simple-backend) x y) + (backend-write b (%cursor-move x y))) + (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)) + (declare (ignore fg bg bold italic underline reverse dim blink)) + (backend-write b (concatenate 'string (%cursor-move x y) string))) (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 #\-) @@ -55,10 +84,8 @@ POS is :top-left, :top-right, :bottom-left, :bottom-right, (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)) + ;; Top edge + (backend-write b (%cursor-move x y)) (backend-write b (string tl)) (if (and title (plusp (length title))) (let* ((align (or title-align :left)) @@ -83,23 +110,23 @@ POS is :top-left, :top-right, :bottom-left, :bottom-right, (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)) + do (backend-write b (%cursor-move x (+ y i))) (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 (%cursor-move x (+ y height -1))) (backend-write b (string bl)) (backend-write b (make-string (- width 2) :initial-element h)) (backend-write b (string br)))) (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)) + (declare (ignore bg)) + (let ((line (make-string width :initial-element #\space))) + (loop for row from 0 below height + do (backend-write b (%cursor-move x (+ y row))) + (backend-write b line)))) (defmethod draw-link ((b simple-backend) x y string url &key fg bg) @@ -109,7 +136,4 @@ POS is :top-left, :top-right, :bottom-left, :bottom-right, (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 "...")) + (draw-text b x y "..." nil nil))