literate: restructure all 19 org files with per-function blocks and prose
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%.
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
(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."
|
||||
@@ -16,15 +12,11 @@ Returns :modern if COLORTERM contains 'truecolor' or '24bit', nil otherwise."
|
||||
(search "24bit" colorterm :test #'char-equal)))
|
||||
:modern)))
|
||||
|
||||
;;; ─── 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 ─────────────────────────────────────────────────────
|
||||
|
||||
(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."
|
||||
@@ -41,14 +33,12 @@ TIMEOUT seconds. Returns the response string, or nil if no 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))))
|
||||
(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))))
|
||||
|
||||
;;; ─── Orchestrator ───────────────────────────────────────────────────────────
|
||||
|
||||
(defun detect-backend ()
|
||||
"Auto-detect the appropriate backend for the current terminal.
|
||||
Returns a backend instance (modern-backend or simple-backend).
|
||||
|
||||
@@ -11,15 +11,11 @@
|
||||
(fiveam:explain! result)
|
||||
(uiop:quit 0)))
|
||||
|
||||
;; ── Constructor ────────────────────────────────────────────────
|
||||
|
||||
(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))))
|
||||
|
||||
;; ── Escape Generation ──────────────────────────────────────────
|
||||
|
||||
(test sgr-truecolor-foreground
|
||||
"SGR truecolor foreground escape is correct"
|
||||
(is (equal (cl-tty.backend::sgr-fg "#FFD700")
|
||||
@@ -44,8 +40,6 @@
|
||||
(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 ─────────────────────────────────────────────────────
|
||||
|
||||
(test cursor-move-escape
|
||||
"cursor-move generates correct CSI escape"
|
||||
(let ((b (make-modern-backend)))
|
||||
@@ -70,23 +64,17 @@
|
||||
(is (equal (cl-tty.backend::cursor-style-escape :underline t)
|
||||
(format nil "~C[5 q" #\Esc)))))
|
||||
|
||||
;; ── Synchronization ────────────────────────────────────────────
|
||||
|
||||
(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 Hyperlinks ──────────────────────────────────────────
|
||||
|
||||
(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\\\\"
|
||||
(format nil "~C]8;;http://example.com~C\\click here~C]8;;~C\"
|
||||
#\Esc #\Esc #\Esc #\Esc))))
|
||||
|
||||
;; ── Hex Parsing ────────────────────────────────────────────────
|
||||
|
||||
(test hex-color-parsing
|
||||
"hex-to-rgb parses valid hex colors"
|
||||
(multiple-value-bind (r g b) (cl-tty.backend::hex-to-rgb "#FFD700")
|
||||
@@ -108,17 +96,15 @@
|
||||
(is (= g 0))
|
||||
(is (= b 0))))
|
||||
|
||||
;; ── Border Characters ──────────────────────────────────────────
|
||||
|
||||
(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) "╯")))
|
||||
(is (equal (cl-tty.backend::border-char :rounded :bottom-right) "╯"))
|
||||
|
||||
(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) "║")))
|
||||
(is (equal (cl-tty.backend::border-char :double :vertical) "║"))
|
||||
|
||||
@@ -90,7 +90,7 @@ as a fallback when a keyword is not in *named-colors*.")
|
||||
|
||||
(defun osc8-link (url text)
|
||||
"Wrap TEXT in an OSC 8 hyperlink to URL."
|
||||
(format nil "~C]8;;~A~C\\\\~A~C]8;;~C\\\\"
|
||||
(format nil "~C]8;;~A~C\\~A~C]8;;~C\\"
|
||||
#\Esc url #\Esc text #\Esc #\Esc))
|
||||
|
||||
(defparameter *border-chars*
|
||||
|
||||
@@ -6,16 +6,12 @@
|
||||
(def-suite backend-suite :description "Backend protocol tests")
|
||||
(in-suite backend-suite)
|
||||
|
||||
;; ── Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
(defun make-capturing-backend ()
|
||||
"Create a simple-backend that writes to a string stream."
|
||||
(let* ((s (make-string-output-stream))
|
||||
(b (make-simple-backend :output-stream s)))
|
||||
(values b s)))
|
||||
|
||||
;; ── Simple Backend ──────────────────────────────────────────────
|
||||
|
||||
(defun run-tests ()
|
||||
"Run all backend tests."
|
||||
(let ((result (run 'backend-suite)))
|
||||
@@ -46,7 +42,7 @@
|
||||
(draw-border b 0 0 5 3 :style :single)
|
||||
(shutdown-backend b)
|
||||
(let ((out (get-output-stream-string s)))
|
||||
(is (search "+---+" out) "top edge should have +---+")
|
||||
(is (search "+---+" out) "top edge should have +---+\"")
|
||||
(is (search "| |" out) "middle row should have pipe sides"))))
|
||||
|
||||
(test simple-backend-draw-rounded
|
||||
@@ -56,7 +52,7 @@
|
||||
(draw-border b 0 0 5 3 :style :rounded)
|
||||
(shutdown-backend b)
|
||||
(let ((out (get-output-stream-string s)))
|
||||
;; Rounded falls back to ASCII — identical output to single
|
||||
;; Rounded falls back to ASCII -- identical output to single
|
||||
(is (search "+---+" out) "rounded style produces same dashes as single"))))
|
||||
|
||||
(test simple-backend-draw-link
|
||||
@@ -77,8 +73,6 @@
|
||||
(is (string= (get-output-stream-string s) "...")
|
||||
"ellipsis should output 3 dots")))
|
||||
|
||||
;; ── Backend Capabilities ───────────────────────────────────────
|
||||
|
||||
(test capable-p-known-features
|
||||
"capable-p returns nil for all features on simple-backend"
|
||||
(let ((b (make-simple-backend)))
|
||||
@@ -89,8 +83,6 @@
|
||||
(format nil "~s should not be supported on simple-backend" f)))
|
||||
(shutdown-backend b)))
|
||||
|
||||
;; ── Backend Size ───────────────────────────────────────────────
|
||||
|
||||
(test backend-size-returns-integers
|
||||
"backend-size returns two integer values"
|
||||
(let ((b (make-simple-backend)))
|
||||
@@ -102,8 +94,6 @@
|
||||
(is (>= lines 3)))
|
||||
(shutdown-backend b)))
|
||||
|
||||
;; ── Backend Protocol: Defaults and No-ops ──────────────────────
|
||||
|
||||
(test default-methods-are-no-ops
|
||||
"Default backend methods don't error"
|
||||
(let ((b (make-simple-backend)))
|
||||
@@ -126,8 +116,6 @@
|
||||
(is (string= (get-output-stream-string s) "in sync")
|
||||
"no sync escape sequences should appear")))
|
||||
|
||||
;; ── Draw-rect ──────────────────────────────────────────────────
|
||||
|
||||
(test draw-rect-fills-area-correctly
|
||||
"draw-rect with background writes nothing to output (simple-backend no-op)"
|
||||
(multiple-value-bind (b s) (make-capturing-backend)
|
||||
@@ -137,8 +125,6 @@
|
||||
(is (string= (get-output-stream-string s) "")
|
||||
"draw-rect is a no-op on simple-backend")))
|
||||
|
||||
;; ── Detection ──────────────────────────────────────────────────
|
||||
|
||||
(test detection-returns-backend-instance
|
||||
"detect-backend returns a valid backend instance"
|
||||
(let ((be (cl-tty.backend:detect-backend)))
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
(b (make-modern-backend :output-stream s)))
|
||||
(values b s)))
|
||||
|
||||
;; ── Box Tests ─────────────────────────────────────────────────
|
||||
|
||||
(test box-creates-with-defaults
|
||||
"A box created with no arguments has reasonable defaults"
|
||||
(let ((b (make-box)))
|
||||
@@ -92,8 +90,6 @@
|
||||
(let ((out (get-output-stream-string s)))
|
||||
(is (search "┌" out) "2x2 box still has borders")))))
|
||||
|
||||
;; ── Text and Span Tests ───────────────────────────────────────
|
||||
|
||||
(test text-creates-with-defaults
|
||||
"A text created with no arguments has reasonable defaults"
|
||||
(let ((txt (make-text "")))
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
;;; dialog.lisp — Dialog System + Toast for cl-tty
|
||||
|
||||
(in-package :cl-tty.dialog)
|
||||
|
||||
;; ─── Special variables ────────────────────────────────────────────────────────
|
||||
|
||||
(defvar *dialog-stack* nil
|
||||
"Stack of active dialogs. (list) of dialog instances.")
|
||||
|
||||
(defvar *toasts* nil
|
||||
"List of active toast notifications.")
|
||||
|
||||
;; ─── Dialog class ─────────────────────────────────────────────────────────────
|
||||
|
||||
(defclass dialog ()
|
||||
((title :initarg :title :accessor dialog-title)
|
||||
(size :initarg :size :initform :medium :accessor dialog-size)
|
||||
@@ -53,8 +47,6 @@
|
||||
(funcall (dialog-on-dismiss dialog)))
|
||||
dialog)))
|
||||
|
||||
;; ─── Dialog sub-classes ──────────────────────────────────────────────────────
|
||||
|
||||
(defun alert-dialog (title message)
|
||||
(make-instance 'dialog
|
||||
:title title
|
||||
@@ -96,8 +88,6 @@
|
||||
(pop-dialog)
|
||||
(when on-submit (funcall on-submit value))))))
|
||||
|
||||
;; ─── Toast system ─────────────────────────────────────────────────────────────
|
||||
|
||||
(defclass toast ()
|
||||
((message :initarg :message :accessor toast-message)
|
||||
(variant :initarg :variant :initform :info :accessor toast-variant)))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
;; Dirty tracking tests are in box-tests.lisp (same test suite)
|
||||
(in-package :cl-tty-box-test)
|
||||
(in-suite box-suite)
|
||||
|
||||
@@ -7,12 +6,18 @@
|
||||
(let ((c (make-instance 'dirty-mixin)))
|
||||
(is-true (dirty-p c) "new component should be dirty")))
|
||||
|
||||
(in-package :cl-tty-box-test)
|
||||
(in-suite box-suite)
|
||||
|
||||
(test mark-clean-clears-dirty
|
||||
"mark-clean sets dirty to nil"
|
||||
(let ((c (make-instance 'dirty-mixin)))
|
||||
(mark-clean c)
|
||||
(is-false (dirty-p c) "after mark-clean, should not be dirty")))
|
||||
|
||||
(in-package :cl-tty-box-test)
|
||||
(in-suite box-suite)
|
||||
|
||||
(test mark-dirty-sets-dirty
|
||||
"mark-dirty sets dirty to t"
|
||||
(let ((c (make-instance 'dirty-mixin)))
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
;; This file is deprecated. Tests moved to tests/input-tests.lisp.
|
||||
;; Kept as placeholder to prevent confusion with stale copies.
|
||||
(defpackage :cl-tty-input-test
|
||||
(:use :cl :fiveam :cl-tty.backend :cl-tty.box :cl-tty.layout :cl-tty.input)
|
||||
(:export #:run-tests))
|
||||
(in-package :cl-tty-input-test)
|
||||
|
||||
(defun run-tests ()
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
(in-package #:cl-tty.input)
|
||||
|
||||
(defun %split-string (string separator)
|
||||
"Split STRING at each occurrence of SEPARATOR. Returns list of strings."
|
||||
(loop with start = 0
|
||||
for pos = (position separator string :start start)
|
||||
collect (subseq string start pos)
|
||||
while pos
|
||||
do (setf start (1+ pos))))
|
||||
|
||||
(defvar *current-backend* nil
|
||||
"The active backend used for rendering.")
|
||||
|
||||
(defvar *current-theme* nil
|
||||
"The active theme used for semantic color resolution.")
|
||||
|
||||
(defstruct key-event
|
||||
(key nil :type (or keyword null))
|
||||
(ctrl nil :type boolean)
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
(in-package #:cl-tty.input)
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Key map struct
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defstruct keymap
|
||||
(name nil :type (or keyword null))
|
||||
(bindings nil :type list)
|
||||
(parent nil :type (or keymap null)))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Global keymap registry
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defparameter *keymaps* (make-hash-table :test #'equal))
|
||||
|
||||
(defparameter *chord-timeout* 0.5)
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Key spec matching
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun key-match-p (spec event)
|
||||
"T if SPEC matches EVENT. Spec is :ctrl+p (modifier+key keyword)
|
||||
or (:ctrl+p) for single-spec in a list, or (:ctrl+x :ctrl+s) for chords."
|
||||
@@ -26,7 +18,7 @@
|
||||
(let* ((name (string spec))
|
||||
(plus (position #\+ name)))
|
||||
(if plus
|
||||
;; Modified key: :ctrl+p → mod-str="CTRL", key-str="P"
|
||||
;; Modified key: :ctrl+p -> mod-str="CTRL", key-str="P"
|
||||
(let ((mod-str (subseq name 0 plus))
|
||||
(key-str (subseq name (1+ plus))))
|
||||
(and (eql (intern key-str :keyword)
|
||||
@@ -43,24 +35,6 @@
|
||||
(when spec
|
||||
(key-match-p (first spec) event)))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Dispatch
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; dispatch-key-event — main entry point for keymap-based dispatch.
|
||||
;;;
|
||||
;;; IMPORTANT: This function is NOT called by the demo's event loop
|
||||
;;; or by any built-in widget event handlers. Users who want to use
|
||||
;;; the keymap system MUST call dispatch-key-event explicitly in their
|
||||
;;; own event loops, e.g.:
|
||||
;;;
|
||||
;;; (defun handle-event (event)
|
||||
;;; (or (dispatch-key-event event)
|
||||
;;; (handle-text-input my-input event)
|
||||
;;; ...))
|
||||
;;;
|
||||
;;; Chords ((:ctrl+x :ctrl+s)) are not yet supported; only single
|
||||
;;; key specs work. The *chord-timeout* and list-of-lists syntax
|
||||
;;; are reserved for future implementation.
|
||||
(defun dispatch-key-event (event &key component)
|
||||
(labels ((try-keymap (km)
|
||||
(when km
|
||||
@@ -76,9 +50,6 @@
|
||||
(try-keymap (find-keymap :local))
|
||||
(try-keymap (find-keymap :global)))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; defkeymap macro
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defmacro defkeymap (name &body bindings)
|
||||
`(setf (gethash ',name *keymaps*)
|
||||
(make-keymap :name ',name
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
(in-package :cl-tty.markdown)
|
||||
|
||||
;; ─── Node constructors ────────────────────────────────────────────────────────
|
||||
|
||||
(defun make-md-node (type &key children properties content url)
|
||||
(let ((node (list :type type)))
|
||||
(when children (setf (getf node :children) children))
|
||||
@@ -28,8 +26,6 @@
|
||||
(mapcar #'md-node-text (getf node :children))))
|
||||
(t ""))))
|
||||
|
||||
;; ─── Block-level parser ───────────────────────────────────────────────────────
|
||||
|
||||
(defun split-string-into-lines (string)
|
||||
(unless string (return-from split-string-into-lines (coerce nil 'vector)))
|
||||
(let ((result nil) (start 0))
|
||||
@@ -250,8 +246,6 @@
|
||||
(t (incf i)))))
|
||||
(nreverse nodes)))
|
||||
|
||||
;; ─── Inline parser ────────────────────────────────────────────────────────────
|
||||
|
||||
(defun parse-inline (text)
|
||||
(unless (and text (> (length text) 0)) (return-from parse-inline nil))
|
||||
(let ((nodes nil) (i 0) (len (length text)))
|
||||
@@ -348,8 +342,6 @@
|
||||
:url (subseq text (+ close-bracket 2) close-paren))
|
||||
(1+ close-paren)))))
|
||||
|
||||
;; ─── Syntax highlighting ──────────────────────────────────────────────────────
|
||||
|
||||
(defun get-highlighter (lang)
|
||||
(cdr (assoc lang
|
||||
'(("lisp" . (:comment (";" "#|" ";;") :string ("\"")
|
||||
@@ -525,8 +517,6 @@
|
||||
(defun apply-highlight-style (char-vector)
|
||||
(coerce char-vector 'string))
|
||||
|
||||
;; ─── Diff rendering ───────────────────────────────────────────────────────────
|
||||
|
||||
(defun string-prefix-p (prefix string)
|
||||
(and (>= (length string) (length prefix))
|
||||
(string= prefix (subseq string 0 (length prefix)))))
|
||||
@@ -539,8 +529,6 @@
|
||||
((string-prefix-p "-" line) :removed)
|
||||
(t :context)))
|
||||
|
||||
;; ─── Rendering ────────────────────────────────────────────────────────────────
|
||||
|
||||
(defun apply-style (style text)
|
||||
(let ((code (cond
|
||||
((eql style :bold) "1") ((eql style :italic) "3")
|
||||
|
||||
@@ -39,7 +39,6 @@ Components without a layout-node or position return nil."
|
||||
node)))))))
|
||||
(recurse root)))
|
||||
|
||||
;; Selection
|
||||
(defvar *selection* nil)
|
||||
|
||||
(defstruct (selection (:conc-name sel-))
|
||||
@@ -58,8 +57,6 @@ Components without a layout-node or position return nil."
|
||||
:input text :wait nil)))
|
||||
#+darwin (sb-ext:run-program "pbcopy" nil :input text :wait nil))
|
||||
|
||||
;;; --- Selection tracking (mouse drag) ---------------------------------------
|
||||
|
||||
(defvar *selection-active* nil
|
||||
"T when a drag selection is in progress.")
|
||||
|
||||
@@ -98,8 +95,6 @@ Components without a layout-node or position return nil."
|
||||
(setf *selection-start* nil *selection-end* nil)
|
||||
text)))
|
||||
|
||||
;;; --- Link clicking ---------------------------------------------------------
|
||||
|
||||
(defun cell-link-at (fb x y)
|
||||
"Return the link URL at (X Y) in framebuffer FB, or nil."
|
||||
(cl-tty.rendering:fb-cell-link-url fb x y))
|
||||
|
||||
@@ -7,24 +7,30 @@
|
||||
#:box-border-style #:box-title #:box-title-align
|
||||
#:box-fg #:box-bg
|
||||
#:render-box
|
||||
|
||||
;; Span
|
||||
#:span
|
||||
#:span-text #:span-bold #:span-italic #:span-underline
|
||||
#:span-reverse #:span-dim #:span-fg #:span-bg
|
||||
|
||||
;; Text
|
||||
#:text #:make-text
|
||||
#:text-layout-node #:text-content #:text-spans
|
||||
#:text-fg #:text-bg #:text-wrap-mode
|
||||
#:render-text
|
||||
|
||||
;; Utilities (for tests)
|
||||
#:word-wrap #:split-string
|
||||
|
||||
;; Dirty tracking
|
||||
#:dirty-mixin #:dirty-p #:mark-clean #:mark-dirty
|
||||
|
||||
;; Rendering pipeline
|
||||
#:render #:render-screen #:render-node
|
||||
#:component-layout-node #:component-children #:component-parent
|
||||
#:available-width #:available-height
|
||||
#:propagate-dirty
|
||||
|
||||
;; Theme engine
|
||||
#:theme #:make-theme #:theme-mode
|
||||
#:theme-color #:load-preset #:define-preset))
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
;; ── Component Protocol ────────────────────────────────────────
|
||||
|
||||
(defgeneric component-layout-node (component)
|
||||
(:documentation "Return the layout-node for COMPONENT.")
|
||||
(:method ((bx box)) (box-layout-node bx))
|
||||
(:method ((tx text)) (text-layout-node tx)))
|
||||
(:documentation "Return the layout-node for COMPONENT."))
|
||||
|
||||
(defmethod component-layout-node ((bx box))
|
||||
(box-layout-node bx))
|
||||
|
||||
(defmethod component-layout-node ((tx text))
|
||||
(text-layout-node tx))
|
||||
|
||||
(defgeneric component-children (component)
|
||||
(:documentation "Return the children of COMPONENT, or nil.")
|
||||
|
||||
@@ -1,77 +1,120 @@
|
||||
(in-package #:cl-tty.select)
|
||||
|
||||
(defclass select (dirty-mixin)
|
||||
((options :initform nil :initarg :options :accessor select-options :type list)
|
||||
(filter :initform nil :initarg :filter :accessor select-filter :type (or string null))
|
||||
(selected-index :initform 0 :initarg :selected-index :accessor select-selected-index :type fixnum)
|
||||
(on-select :initform nil :initarg :on-select :accessor select-on-select)
|
||||
(layout-node :initform (make-layout-node) :initarg :layout-node :accessor select-layout-node)))
|
||||
((options :initform nil :initarg :options
|
||||
:accessor select-options :type list)
|
||||
(filter :initform nil :initarg :filter
|
||||
:accessor select-filter :type (or string null))
|
||||
(selected-index :initform 0 :initarg :selected-index
|
||||
:accessor select-selected-index :type fixnum)
|
||||
(on-select :initform nil :initarg :on-select
|
||||
:accessor select-on-select)
|
||||
(layout-node :initform (make-layout-node) :initarg :layout-node
|
||||
:accessor select-layout-node)))
|
||||
|
||||
(defun make-select (&key options filter on-select)
|
||||
(make-instance 'select :options (or options nil) :filter filter :on-select on-select))
|
||||
(make-instance 'select
|
||||
:options (or options nil)
|
||||
:filter filter
|
||||
:on-select on-select))
|
||||
|
||||
(defmethod component-layout-node ((sel select)) (select-layout-node sel))
|
||||
(defmethod component-layout-node ((sel select))
|
||||
(select-layout-node sel))
|
||||
|
||||
(defun select-filtered-options (sel)
|
||||
(let* ((filter (select-filter sel)) (all-options (select-options sel))
|
||||
(filtered (if (null filter) all-options
|
||||
"Return list of options matching the current filter, in display order.
|
||||
Each item: (display-index original-index option-plist)."
|
||||
(let* ((filter (select-filter sel))
|
||||
(all-options (select-options sel))
|
||||
(filtered (if (null filter)
|
||||
all-options
|
||||
(let ((lower (string-downcase filter)))
|
||||
(remove-if-not
|
||||
(lambda (opt)
|
||||
(or (getf opt :category)
|
||||
(let ((title (string-downcase (getf opt :title))))
|
||||
(or (search lower title) (fuzzy-match-p lower title)))))
|
||||
(or (search lower title)
|
||||
(fuzzy-match-p lower title)))))
|
||||
all-options)))))
|
||||
(loop for opt in filtered for i from 0
|
||||
(loop for opt in filtered
|
||||
for i from 0
|
||||
collect (list i (position opt all-options) opt))))
|
||||
|
||||
(defun fuzzy-match-p (query target)
|
||||
(let* ((q (remove-duplicates (coerce (string-downcase query) 'list)))
|
||||
(tg (remove-duplicates (coerce (string-downcase target) 'list)))
|
||||
(intersection (length (intersection q tg)))
|
||||
(union (length (union q tg))))
|
||||
"T if character-set Jaccard similarity exceeds threshold (0.3)."
|
||||
(let* ((q-chars (remove-duplicates (coerce (string-downcase query) 'list)))
|
||||
(t-chars (remove-duplicates (coerce (string-downcase target) 'list)))
|
||||
(intersection (length (intersection q-chars t-chars)))
|
||||
(union (length (union q-chars t-chars))))
|
||||
(if (zerop union) nil (> (/ (float intersection) union) 0.3))))
|
||||
|
||||
(defun select-clamp-index (sel)
|
||||
(let* ((filtered (select-filtered-options sel)) (count (length filtered)))
|
||||
(if (zerop count) (setf (select-selected-index sel) 0)
|
||||
(setf (select-selected-index sel) (max 0 (min (select-selected-index sel) (1- count)))))))
|
||||
"Ensure selected-index is valid. Wraps if empty."
|
||||
(let* ((filtered (select-filtered-options sel))
|
||||
(count (length filtered)))
|
||||
(if (zerop count)
|
||||
(setf (select-selected-index sel) 0)
|
||||
(setf (select-selected-index sel)
|
||||
(max 0 (min (select-selected-index sel) (1- count)))))))
|
||||
|
||||
(defun select-next (sel)
|
||||
(let* ((filtered (select-filtered-options sel)) (count (length filtered))
|
||||
"Move selection to next non-category option. Wraps at end."
|
||||
(let* ((filtered (select-filtered-options sel))
|
||||
(count (length filtered))
|
||||
(current (select-selected-index sel)))
|
||||
(when (plusp count)
|
||||
(loop for i from 1 below count
|
||||
for idx = (mod (+ current i) count)
|
||||
for opt = (third (nth idx filtered))
|
||||
when (not (getf opt :category))
|
||||
do (setf (select-selected-index sel) idx) (mark-dirty sel) (return)))))
|
||||
do (setf (select-selected-index sel) idx)
|
||||
(mark-dirty sel)
|
||||
(return)))))
|
||||
|
||||
(defun select-prev (sel)
|
||||
(let* ((filtered (select-filtered-options sel)) (count (length filtered))
|
||||
"Move selection to previous non-category option. Wraps at start."
|
||||
(let* ((filtered (select-filtered-options sel))
|
||||
(count (length filtered))
|
||||
(current (select-selected-index sel)))
|
||||
(when (plusp count)
|
||||
(loop for i from 1 below count
|
||||
for idx = (mod (- current i) count)
|
||||
for opt = (third (nth idx filtered))
|
||||
when (not (getf opt :category))
|
||||
do (setf (select-selected-index sel) idx) (mark-dirty sel) (return)))))
|
||||
do (setf (select-selected-index sel) idx)
|
||||
(mark-dirty sel)
|
||||
(return)))))
|
||||
|
||||
(defun select-handle-key (sel event)
|
||||
(let ((key (key-event-key event)) (ctrl (key-event-ctrl event)))
|
||||
"Handle a key-event. Returns T if handled."
|
||||
(let ((key (key-event-key event))
|
||||
(ctrl (key-event-ctrl event)))
|
||||
(cond
|
||||
((or (eql key :down) (and ctrl (eql key :n))) (select-next sel) t)
|
||||
((or (eql key :up) (and ctrl (eql key :p))) (select-prev sel) t)
|
||||
((or (eql key :down) (and ctrl (eql key :n)))
|
||||
(select-next sel) t)
|
||||
((or (eql key :up) (and ctrl (eql key :p)))
|
||||
(select-prev sel) t)
|
||||
((eql key :enter)
|
||||
(let* ((filtered (select-filtered-options sel)) (idx (select-selected-index sel))
|
||||
(item (when (< idx (length filtered)) (third (nth idx filtered)))))
|
||||
(when item (let ((cb (select-on-select sel))) (when cb (funcall cb item)))) t))
|
||||
((eql key :escape) nil) (t nil))))
|
||||
(let* ((filtered (select-filtered-options sel))
|
||||
(idx (select-selected-index sel))
|
||||
(item (when (< idx (length filtered))
|
||||
(third (nth idx filtered)))))
|
||||
(when item
|
||||
(let ((cb (select-on-select sel)))
|
||||
(when cb (funcall cb item))))
|
||||
t))
|
||||
((eql key :escape) nil)
|
||||
(t nil))))
|
||||
|
||||
(defun select-visible-options (sel)
|
||||
(let* ((ln (select-layout-node sel)) (height (if ln (layout-node-height ln) 80))
|
||||
(filtered (select-filtered-options sel)) (sel-idx (select-selected-index sel))
|
||||
(half (floor (1- height) 2)) (start (max 0 (- sel-idx half)))
|
||||
"Return filtered options that fit within the viewport."
|
||||
(let* ((ln (select-layout-node sel))
|
||||
(height (if ln (layout-node-height ln) 80))
|
||||
(filtered (select-filtered-options sel))
|
||||
(sel-idx (select-selected-index sel))
|
||||
;; Show items around the selection
|
||||
(half (floor (1- height) 2))
|
||||
(start (max 0 (- sel-idx half)))
|
||||
(end (min (length filtered) (+ start height))))
|
||||
(subseq filtered start end)))
|
||||
|
||||
@@ -80,17 +123,24 @@
|
||||
(x (if ln (layout-node-x ln) 0))
|
||||
(y (if ln (layout-node-y ln) 0))
|
||||
(w (if ln (layout-node-width ln) 80))
|
||||
(visible (select-visible-options sel)) (sel-idx (select-selected-index sel)))
|
||||
(visible (select-visible-options sel))
|
||||
(sel-idx (select-selected-index sel)))
|
||||
(dolist (item visible)
|
||||
(let* ((display-idx (first item)) (option (third item))
|
||||
(title (getf option :title)) (cat (getf option :category))
|
||||
(selected (eql display-idx sel-idx))
|
||||
(let* ((display-idx (first item))
|
||||
(option (third item))
|
||||
(title (getf option :title))
|
||||
(is-category (getf option :category))
|
||||
(is-selected (eql display-idx sel-idx))
|
||||
(display (if (> (length title) (1- w))
|
||||
(concatenate 'string (subseq title 0 (1- w)) "…") title)))
|
||||
(cond (cat (draw-text backend x y display :text-muted nil))
|
||||
(selected
|
||||
(draw-rect backend x y w 1 :bg :accent)
|
||||
(draw-text backend x y display :background :accent))
|
||||
(t (draw-text backend x y display nil nil)))
|
||||
(concatenate 'string (subseq title 0 (1- w)) "…")
|
||||
title)))
|
||||
(cond
|
||||
(is-category
|
||||
(draw-text backend x y display :text-muted nil))
|
||||
(is-selected
|
||||
(draw-rect backend x y w 1 :bg :accent)
|
||||
(draw-text backend x y display :background :accent))
|
||||
(t
|
||||
(draw-text backend x y display nil nil)))
|
||||
(incf y 1)))
|
||||
(values)))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
(in-package :cl-tty.box)
|
||||
|
||||
;; ── Text Renderable ────────────────────────────────────────────
|
||||
|
||||
(defclass span ()
|
||||
((text :initarg :text :accessor span-text)
|
||||
(bold :initform nil :initarg :bold :accessor span-bold)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
(in-package #:cl-tty.input)
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Textarea class
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defclass textarea (dirty-mixin)
|
||||
((value :initform "" :initarg :value :accessor textarea-value :type string)
|
||||
(cursor-row :initform 0 :accessor textarea-cursor-row :type fixnum)
|
||||
@@ -21,9 +18,6 @@
|
||||
:value (or value "")
|
||||
:on-submit on-submit))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Line helpers
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun textarea-lines (ta)
|
||||
"Split value into lines."
|
||||
(%split-string (textarea-value ta) #\Newline))
|
||||
@@ -42,9 +36,6 @@
|
||||
(max 0 (min (textarea-cursor-col ta) line-len)))))
|
||||
(mark-dirty ta))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Utility: join strings with newline
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun %join-lines (lines)
|
||||
"Join a sequence of strings with newlines."
|
||||
(with-output-to-string (s)
|
||||
@@ -53,9 +44,6 @@
|
||||
do (unless first (write-char #\Newline s))
|
||||
(write-string line s))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Text manipulation
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun textarea-insert-char (ta char)
|
||||
"Insert CHAR at the cursor position."
|
||||
(textarea-push-undo ta)
|
||||
@@ -141,9 +129,6 @@
|
||||
(decf (textarea-cursor-col ta))
|
||||
(mark-dirty ta))))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Cursor movement
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun textarea-move-up (ta)
|
||||
(decf (textarea-cursor-row ta))
|
||||
(textarea-ensure-cursor ta))
|
||||
@@ -152,9 +137,6 @@
|
||||
(incf (textarea-cursor-row ta))
|
||||
(textarea-ensure-cursor ta))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Undo/redo
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun textarea-push-undo (ta)
|
||||
"Save current value on undo stack."
|
||||
(let ((stack (textarea-undo-stack ta)))
|
||||
@@ -183,9 +165,6 @@
|
||||
(textarea-ensure-cursor ta)
|
||||
(mark-dirty ta)))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Key event handler
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun handle-textarea-input (ta event)
|
||||
"Process a key-event on a textarea widget."
|
||||
(cond
|
||||
@@ -239,9 +218,6 @@
|
||||
(when (and ch (graphic-char-p ch))
|
||||
(textarea-insert-char ta ch))))))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Rendering
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defmethod render ((ta textarea) (backend t))
|
||||
"Render textarea lines at layout position."
|
||||
(let* ((ln (textarea-layout-node ta))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
(in-package :cl-tty.box)
|
||||
|
||||
;; ── Theme Engine ──────────────────────────────────────────────
|
||||
|
||||
(defclass theme ()
|
||||
((mode :initform :dark :initarg :mode :accessor theme-mode)
|
||||
(roles :initform (make-hash-table) :accessor theme-roles)))
|
||||
|
||||
@@ -119,8 +119,6 @@
|
||||
(is (= (layout-node-y (elt sc 0)) 0))
|
||||
(is (= (layout-node-y (elt sc 1)) 3)))))
|
||||
|
||||
;; ── Edge Cases ────────────────────────────────────────────────
|
||||
|
||||
(test empty-container-does-not-crash
|
||||
(let ((r (make-layout-node)))
|
||||
(compute-layout r 20 20)
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
(in-package :cl-tty.rendering)
|
||||
|
||||
;;; ─── Cell — immutable per-cell state ─────────────────────────────────────────
|
||||
|
||||
(defstruct cell
|
||||
"A single terminal cell — character, colors, and attributes."
|
||||
(char #\space :type character)
|
||||
@@ -24,8 +22,6 @@
|
||||
(underline nil :type boolean)
|
||||
(link-url nil))
|
||||
|
||||
;;; ─── Framebuffer — 2D array of cells ────────────────────────────────────────
|
||||
|
||||
(defun make-framebuffer (width height)
|
||||
"Create a 2D array of CELL with dimensions HEIGHT x WIDTH."
|
||||
(make-array (list height width)
|
||||
@@ -40,8 +36,6 @@
|
||||
"Return the height (rows) of framebuffer FB."
|
||||
(if (arrayp fb) (array-dimension fb 0) 0))
|
||||
|
||||
;;; ─── Framebuffer Backend — implements backend protocol ─────────────────────
|
||||
|
||||
(defclass framebuffer-backend (backend)
|
||||
((framebuffer :initform nil :accessor fb-framebuffer)
|
||||
(scissor-x :initform 0 :accessor fb-scissor-x)
|
||||
@@ -55,8 +49,6 @@
|
||||
(setf (fb-framebuffer fb) (make-framebuffer width height))
|
||||
fb))
|
||||
|
||||
;;; ─── Drawing methods ─────────────────────────────────────────────────────────
|
||||
|
||||
(defun %in-scissor-p (fb cx cy)
|
||||
"Check if (CX, CY) falls within the current scissor rectangle."
|
||||
(let ((sx (fb-scissor-x fb)) (sy (fb-scissor-y fb))
|
||||
@@ -129,8 +121,6 @@
|
||||
(dotimes (i (min 3 width))
|
||||
(%set-cell fb (+ x i) y #\. :fg fg :bg bg)))
|
||||
|
||||
;;; ─── Diff ────────────────────────────────────────────────────────────────────
|
||||
|
||||
(defun cells-equal-p (a b)
|
||||
"Return T if two cells have identical content and style."
|
||||
(and (eql (cell-char a) (cell-char b))
|
||||
@@ -153,8 +143,6 @@
|
||||
(push (list x y b) changes)))))
|
||||
(nreverse changes)))
|
||||
|
||||
;;; ─── Flush ───────────────────────────────────────────────────────────────────
|
||||
|
||||
(defun flush-framebuffer (prev-fb curr-fb backend)
|
||||
"Diff PREV-FB and CURR-FB and flush changes to BACKEND.
|
||||
Returns the number of changed cells."
|
||||
@@ -176,8 +164,6 @@ Returns the number of changed cells."
|
||||
(end-sync backend))
|
||||
count))
|
||||
|
||||
;;; --- Frame inspection ---------------------------------------------------
|
||||
|
||||
(defun fb-cell-link-url (fb x y)
|
||||
"Return the link URL at (X Y) in framebuffer FB, or nil."
|
||||
(when (and (arrayp fb) (>= y 0) (< y (array-dimension fb 0))
|
||||
@@ -198,8 +184,6 @@ Returns the number of changed cells."
|
||||
(princ (cell-char c) s)))
|
||||
(when (< y y-max) (princ #\Newline s))))))
|
||||
|
||||
;;; ─── Scissor clipping ────────────────────────────────────────────────────────
|
||||
|
||||
(defmacro with-scissor ((fb x y w h) &body body)
|
||||
"Clip all drawing on FB to rectangle (X Y W H)."
|
||||
(let ((old-x (gensym)) (old-y (gensym))
|
||||
|
||||
Reference in New Issue
Block a user