Files
cl-tty/docs/plans/2026-05-11-terminal-detection.md
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

6.7 KiB

Terminal Capability Detection — Implementation Plan

For Hermes: Implement this plan task-by-task using subagent-driven-development.

Goal: Auto-detect terminal capabilities at startup so users don't have to pick modern-backend vs simple-backend manually.

Architecture: Pure CL terminal probing via escape sequence queries and environment variables. No external dependencies. Detection happens once at startup and returns a backend instance.

Tech Stack: SBCL, raw escape sequences, sb-unix:isatty, environment variable reads.


Task 1: Create detection.org literate source

Objective: Write the org file with prose, contract, and tangle blocks for the detection module. No code generation yet — this is the design document.

Files:

  • Create: org/detection.org

Content structure:

#+TITLE: Terminal Capability Detection (v0.12.0)

* Overview
  - Why detection matters
  - Strategy: TTY check → COLORTERM → DA1 query → DA3 query

** Contract
  - detect-backend () → modern-backend or simple-backend
  - detect-backend-by-env () → :modern, :simple, or nil
  - query-terminal-feature (query-string timeout) → string or nil

** Plan (this document — tasks for implementation)

** Tests
  - #+BEGIN_SRC lisp :tangle ../backend/tests.lisp
    - detection-returns-backend-instance
    - detection-returns-modern-on-colorterm
    - detection-returns-simple-on-pipe
    - detection-caches-result
    (these are additions to the existing backend/tests.lisp)

** Implementation
  - Package (adds to cl-tty.backend)
  - Environment probe (COLORTERM)
  - TTY probe (sb-unix:isatty)
  - DA1 probe (terminal queries)
  - detect-backend (orchestrator)
  - Cache (defvar *detected-backend*)

Step 1: Write the org file at org/detection.org with the sections above, full prose, and empty code blocks.

Step 2: Review — verify structure matches existing .org files in the project.

Step 3: Commit

git add org/detection.org
git commit -m "docs: add detection module design and plan"

Task 2: Add detection functions to backend/classes.lisp

Objective: Implement the environment and TTY probe functions.

Files:

  • Modify: backend/classes.lisp (add methods to existing backend classes)

Code to add:

;;; ─── Detection ──────────────────────────────────────────────────────────────

(defvar *detected-backend* nil
  "Cached backend instance from detect-backend.")

(defun detect-backend-by-env ()
  "Check COLORTERM environment variable for modern terminal support."
  (let ((colorterm (sb-ext:posix-getenv "COLORTERM")))
    (when (and colorterm
               (or (search "truecolor" colorterm :test #'char-equal)
                   (search "24bit" colorterm :test #'char-equal)))
      :modern)))

(defun detect-backend-by-tty ()
  "Check if stdout is a real terminal (not a pipe)."
  (sb-unix:isatty sb-sys:*stdout*))

(defun detect-backend ()
  "Auto-detect the appropriate backend for the current terminal.
Returns a backend instance."
  (or *detected-backend*
      (setf *detected-backend*
            (if (and (detect-backend-by-tty)
                     (or (eql (detect-backend-by-env) :modern)
                         t))  ;; TODO: add DA1/DA3 probe here
                (make-modern-backend)
                (make-simple-backend)))))

Test additions 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*)))))

Follow TDD:

  1. Write failing tests in src/components/box-tests.lisp (or wherever backend tests live — actually in backend/tests.lisp)
  2. Run tests to verify failure
  3. Write implementation code in backend/classes.lisp
  4. Run tests to verify pass
  5. Commit

Task 3: Add DA1/DA3 terminal query probe

Objective: Send escape sequence queries to the terminal and parse responses to detect modern features (Kitty keyboard, DECICM sync).

Files:

  • Modify: backend/classes.lisp

Implementation:

(defun query-terminal (query timeout-sec)
  "Send a query string to the terminal and return the response.
Returns nil if no response within TIMEOUT-SEC seconds."
  (let ((response (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t)))
    (format t "~A" query)
    (force-output)
    (sleep timeout-sec)
    (loop while (listen)
          do (vector-push-extend (read-char-no-hang) response))
    (when (plusp (length response))
      response)))

(defun detect-backend-by-da1 ()
  "Send DA1 (Device Attributes) query and parse response for modern features."
  (let ((response (query-terminal (format nil "~C[c" #\Esc) 0.1)))
    (when response
      ;; Check for specific feature codes in response
      (search "?62" response))))  ;; kitty terminal indicator

(defun detect-backend ()
  "Auto-detect the appropriate backend for the current terminal."
  (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)))))

Note: DA1 queries are best-effort — many terminals don't respond or respond asynchronously. The env-var check is more reliable. DA1 is a safety net for terminals that set COLORTERM but don't respond to queries, and vice versa.

Test for DA1 is hard to automate (requires a real terminal). Add a manual test note.


Task 4: Wire into ASDF and run full test suite

Files:

  • Modify: cl-tty.asd (add detection.lisp if created as separate file, or verify existing)
  • Run: run-all-tests.lisp

Steps:

  1. Ensure cl-tty.asd includes the detection code (if in backend/classes.lisp it's already loaded)
  2. Run full test suite: sbcl --script run-all-tests.lisp
  3. Verify all 358+ tests pass (add 2 new detection tests → 360)
  4. Commit

Task 5: Update demo.lisp to use detection

Objective: Make demo.lisp use detect-backend instead of hardcoded make-modern-backend.

Files:

  • Modify: demo.lisp

Change: Replace (make-modern-backend) with (detect-backend).

Verification: sbcl --script demo.lisp should work in a terminal.


Task 6: Tangle org → lisp and verify no regressions

Files: All

Steps:

  1. Tangle all org files: for f in org/*.org; do emacs --batch ...; done
  2. Run full test suite
  3. Verify 0 regressions
  4. Commit final