Files
cl-tty/org/detection.org
Hermes b7df68c436 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
2026-05-11 22:25:42 +00:00

5.8 KiB

Terminal Capability Detection (v0.12.0)

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-backendmodern-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

;; 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*)))))

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.

(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)))

TTY probe

Check if stdout is connected to a terminal (not a pipe or file).

;;; ─── 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*))

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.

;;; ─── 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))))

Orchestrator

Tie all probes together into detect-backend.

;;; ─── 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)))))