v0.12.0: Terminal capability detection, GPL 3.0 license, roadmap rewrite
LICENSE: - Added GNU General Public License v3.0 - Updated README.org to reflect GPL 3.0 ROADMAP: - Complete rewrite to reflect actual project state - Removed croatoan/ncurses/Yoga FFI references - Marked all 11 existing versions DONE - Added v0.12.0-0.14.0 for new features (detection, pipeline, mouse) DETECTION (v0.12.0): - detect-backend: auto-detect modern vs simple backend - detect-backend-by-env: check COLORTERM env var - detect-backend-by-tty: check interactive-stream-p - detect-backend-by-da1: query terminal via ESC[c (best-effort) - *detected-backend* cache for zero-cost subsequent calls - Added detection.lisp to ASDF and package exports - Added 2 new tests (360 total, all passing) - demo.lisp updated to use detect-backend ORG BACKPORT (pre-existing fixes synced): - dialog.org: render-dialog/render-toast fixes, class initforms - scrollbox-tabbar.org: background-element -> bright-black, remove duplicate render - select.org: remove duplicate render export - text-input.org: remove duplicate %split-string, undo overflow fix - layout-engine.org: quoted-literal -> list constructors, normalize-box rewrite - mouse.org: add missing exports, fix test
This commit is contained in:
155
org/detection.org
Normal file
155
org/detection.org
Normal file
@@ -0,0 +1,155 @@
|
||||
#+TITLE: Terminal Capability Detection (v0.12.0)
|
||||
#+DATE: 2026-05-11
|
||||
#+AUTHOR: Amr Gharbeia / Hermes
|
||||
#+STARTUP: content
|
||||
|
||||
* Overview
|
||||
|
||||
Currently, users must manually choose between ~modern-backend~ and
|
||||
~simple-backend~ when initializing cl-tty. This module adds auto-detection:
|
||||
|
||||
1. Check if stdout is a real TTY (not piped/redirected)
|
||||
2. Check the =COLORTERM= environment variable for truecolor support
|
||||
3. Optionally query the terminal via DA1/DA3 escape sequences
|
||||
4. Return the appropriate backend, cached for subsequent calls
|
||||
|
||||
Detection is best-effort: the COLORTERM env var is the most reliable single
|
||||
signal. DA1 queries are asynchronous and many terminals don't respond.
|
||||
If detection can't determine modern capability, it falls back to
|
||||
~simple-backend~.
|
||||
|
||||
** Contract
|
||||
|
||||
- ~detect-backend~ → ~modern-backend~ or ~simple-backend~
|
||||
Auto-detect and return the appropriate backend. Results are cached
|
||||
in ~*detected-backend*~.
|
||||
|
||||
- ~detect-backend-by-env~ → ~:modern~ or ~nil~
|
||||
Check =COLORTERM= env var for ~truecolor~ or ~24bit~.
|
||||
|
||||
- ~detect-backend-by-tty~ → boolean
|
||||
Check if stdout is a real terminal (not a pipe).
|
||||
|
||||
- ~detect-backend-by-da1~ → boolean
|
||||
Send DA1 (~ESC[c~) query and check for modern feature responses.
|
||||
|
||||
- ~*detected-backend*~ — variable
|
||||
Cache for detection result. ~nil~ = not yet detected.
|
||||
|
||||
* Plan
|
||||
|
||||
See =docs/plans/2026-05-11-terminal-detection.md= for implementation tasks.
|
||||
|
||||
1. Create ~detection.lisp~ with all detection functions
|
||||
2. Wire into ASDF
|
||||
3. Update ~demo.lisp~ to use ~detect-backend~
|
||||
4. Tangle, test, commit
|
||||
|
||||
* Tests
|
||||
|
||||
#+BEGIN_SRC lisp :tangle no
|
||||
;; Tests are manually added to backend/tests.lisp
|
||||
(def-test detection-returns-backend-instance ()
|
||||
(let ((be (cl-tty.backend:detect-backend)))
|
||||
(is-true (typep be 'cl-tty.backend:backend))))
|
||||
|
||||
(def-test detection-caches-result ()
|
||||
(let ((*detected-backend* nil))
|
||||
(cl-tty.backend:detect-backend)
|
||||
(is-true (not (null cl-tty.backend::*detected-backend*)))))
|
||||
#+END_SRC
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package
|
||||
|
||||
Detection functions are added to the existing ~cl-tty.backend~ package.
|
||||
No new package definition needed.
|
||||
|
||||
** Environment probe
|
||||
|
||||
Check ~COLORTERM~ first — it's the simplest and most reliable signal.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../backend/detection.lisp
|
||||
(in-package :cl-tty.backend)
|
||||
|
||||
;;; ─── Detection cache ────────────────────────────────────────────────────────
|
||||
|
||||
(defvar *detected-backend* nil
|
||||
"Cached backend instance from detect-backend. Nil = not yet detected.")
|
||||
|
||||
;;; ─── Environment probe ──────────────────────────────────────────────────────
|
||||
|
||||
(defun detect-backend-by-env ()
|
||||
"Check COLORTERM environment variable for modern terminal support.
|
||||
Returns :modern if COLORTERM contains 'truecolor' or '24bit', nil otherwise."
|
||||
(let ((colorterm (sb-ext:posix-getenv "COLORTERM")))
|
||||
(when (and colorterm
|
||||
(or (search "truecolor" colorterm :test #'char-equal)
|
||||
(search "24bit" colorterm :test #'char-equal)))
|
||||
:modern)))
|
||||
#+END_SRC
|
||||
|
||||
** TTY probe
|
||||
|
||||
Check if stdout is connected to a terminal (not a pipe or file).
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../backend/detection.lisp
|
||||
;;; ─── TTY probe ──────────────────────────────────────────────────────────────
|
||||
|
||||
(defun detect-backend-by-tty ()
|
||||
"Check if stdout is a real terminal (not a pipe/redirect).
|
||||
Returns T if stdout is interactive, nil otherwise."
|
||||
(interactive-stream-p *standard-output*))
|
||||
#+END_SRC
|
||||
|
||||
** DA1 terminal query (best-effort)
|
||||
|
||||
Send a DA1 (Device Attributes) query and briefly listen for a response.
|
||||
This is best-effort — many terminals respond asynchronously or not at all.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../backend/detection.lisp
|
||||
;;; ─── DA1 terminal query ─────────────────────────────────────────────────────
|
||||
|
||||
(defun query-terminal (query &optional (timeout 0.1))
|
||||
"Send QUERY string to terminal and return any response received within
|
||||
TIMEOUT seconds. Returns the response string, or nil if no response."
|
||||
(write-string query *query-io*)
|
||||
(force-output *query-io*)
|
||||
(sleep timeout)
|
||||
(let ((response (make-array 0 :element-type 'character
|
||||
:fill-pointer 0 :adjustable t)))
|
||||
(loop while (listen *query-io*)
|
||||
do (vector-push-extend (read-char-no-hang *query-io*) response))
|
||||
(when (plusp (length 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."
|
||||
(let ((response (query-terminal (format nil "~C[c" #\Esc))))
|
||||
(when response
|
||||
;; DA1 response format: ESC [ ? digits ; digits c
|
||||
;; Kitty reports code 62 in the response
|
||||
(search "?62" response))))
|
||||
#+END_SRC
|
||||
|
||||
** Orchestrator
|
||||
|
||||
Tie all probes together into ~detect-backend~.
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../backend/detection.lisp
|
||||
;;; ─── Orchestrator ───────────────────────────────────────────────────────────
|
||||
|
||||
(defun detect-backend ()
|
||||
"Auto-detect the appropriate backend for the current terminal.
|
||||
Returns a backend instance (modern-backend or simple-backend).
|
||||
Result is cached in *detected-backend* for subsequent calls."
|
||||
(or *detected-backend*
|
||||
(setf *detected-backend*
|
||||
(if (and (detect-backend-by-tty)
|
||||
(or (eql (detect-backend-by-env) :modern)
|
||||
(detect-backend-by-da1)))
|
||||
(make-modern-backend)
|
||||
(make-simple-backend)))))
|
||||
#+END_SRC
|
||||
Reference in New Issue
Block a user