v1.0.0 — Stable release + TUI support #8

Merged
amr merged 46 commits from feature/v0.11.0-slots into main 2026-05-12 16:34:48 -04:00
9 changed files with 116 additions and 19 deletions
Showing only changes of commit 3cbcfd2d75 - Show all commits

View File

@@ -2,7 +2,7 @@
(asdf:defsystem :cl-tty (asdf:defsystem :cl-tty
:description "Reusable Common Lisp Terminal UI Framework" :description "Reusable Common Lisp Terminal UI Framework"
:author "Amr Gharbeia" :author "Amr Gharbeia"
:version "0.15.0" :version "1.0.0"
:license "GPL-3.0" :license "GPL-3.0"
:depends-on (:sb-posix) :depends-on (:sb-posix)
:components :components
@@ -71,7 +71,7 @@
(:file "dirty-tests") (:file "dirty-tests")
(:file "render-tests") (:file "render-tests")
(:file "theme-tests") (:file "theme-tests")
(:file "input-tests") (:file "input-tests" :pathname "../../tests/input-tests")
(:file "scrollbox-tabbar-tests" :pathname "../../tests/scrollbox-tabbar-tests") (:file "scrollbox-tabbar-tests" :pathname "../../tests/scrollbox-tabbar-tests")
(:file "select-tests" :pathname "../../tests/select-tests") (:file "select-tests" :pathname "../../tests/select-tests")
(:file "markdown-tests" :pathname "../../tests/markdown-tests") (:file "markdown-tests" :pathname "../../tests/markdown-tests")

View File

@@ -177,22 +177,20 @@ quality-of-life infrastructure.
- Project restructure: move backend/ and layout/ into src/ - Project restructure: move backend/ and layout/ into src/
- .gitignore for compiled fasl files - .gitignore for compiled fasl files
- ~500 lines of changes across the codebase - ~500 lines of changes across the codebase
- Version: v0.15.0 (current) |- Version: v1.0.0 (current)
Known gaps from earlier phases: Known gaps from earlier phases:
- suspend-backend / resume-backend (in ARCHITECTURE.org protocol - (none — all protocol spec items implemented)
spec but never implemented)
- Slot modes (defslot :mode parameter planned but not implemented)
** v1.0.0: Release (target — not yet released) ** v1.0.0: Release
All phases integrated and tested. Applications can build rich terminal UIs DONE. All phases integrated and tested. Applications can build rich terminal UIs
from the component library without writing custom escape sequences. from the component library without writing custom escape sequences.
Checklist: Checklist:
- [X] README.org with overview, architecture, component table, quick start - [X] README.org with overview, architecture, component table, quick start
- [X] demo.lisp — working interactive example - [X] demo.lisp — working interactive example
- [X] Full test suite: 483 checks, 100% passing across 13 suites - [X] Full test suite: 454 checks, 100% passing across 14 suites
- [X] ASDF system with test-op - [X] ASDF system with test-op
- [X] LICENSE file (GPL 3.0) - [X] LICENSE file (GPL 3.0)
- [X] Literate org files for all modules - [X] Literate org files for all modules
@@ -200,8 +198,8 @@ Checklist:
- [X] Rendering pipeline (v0.13.0) - [X] Rendering pipeline (v0.13.0)
- [X] Mouse improvements (v0.14.0) - [X] Mouse improvements (v0.14.0)
- [X] Org/Lisp sync verified (first tangle produces no regressions) - [X] Org/Lisp sync verified (first tangle produces no regressions)
- [ ] Suspend/resume-backend protocol methods (ARCHITECTURE.org spec) - [X] Suspend/resume-backend protocol methods (ARCHITECTURE.org spec)
- [ ] Slot modes (defslot :mode parameter) - [X] Slot modes (defslot :mode parameter)
** Feature Reference ** Feature Reference

View File

@@ -215,7 +215,7 @@ and ~\\ for literal backslash.
(test osc8-escape (test osc8-escape
"OSC 8 hyperlink escape wraps text" "OSC 8 hyperlink escape wraps text"
(is (equal (cl-tty.backend::osc8-link "http://example.com" "click here") (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)))) #\Esc #\Esc #\Esc #\Esc))))
#+END_SRC #+END_SRC
@@ -269,7 +269,7 @@ characters for the four corners and edges.
(is (equal (cl-tty.backend::border-char :rounded :top-left) "╭")) (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 :horizontal) "─"))
(is (equal (cl-tty.backend::border-char :rounded :vertical) "│")) (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) "╯")))
#+END_SRC #+END_SRC
** Border characters --- double style ** Border characters --- double style
@@ -281,7 +281,20 @@ Confirms that =:double= style maps to double-line box-drawing characters.
"modern-border-char returns double-line chars" "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 :top-left) "╔"))
(is (equal (cl-tty.backend::border-char :double :horizontal) "═")) (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) "║")))
#+END_SRC
** Suspend/resume backend
Verifies that suspend-backend and resume-backend are no-ops when called
on a backend not attached to a real terminal (no errors, return nil).
#+BEGIN_SRC lisp :tangle ../src/backend/modern-tests.lisp
(test suspend-resume-noop
"suspend-backend and resume-backend are no-ops in test context"
(let ((b (make-modern-backend)))
(is (null (multiple-value-list (suspend-backend b))))
(is (null (multiple-value-list (resume-backend b))))))
#+END_SRC #+END_SRC
* Implementation * Implementation
@@ -475,7 +488,7 @@ allows clickable text in terminals that support the protocol.
#+BEGIN_SRC lisp :tangle ../src/backend/modern.lisp #+BEGIN_SRC lisp :tangle ../src/backend/modern.lisp
(defun osc8-link (url text) (defun osc8-link (url text)
"Wrap TEXT in an OSC 8 hyperlink to URL." "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)) #\Esc url #\Esc text #\Esc #\Esc))
#+END_SRC #+END_SRC
@@ -586,6 +599,49 @@ leaves the alternate screen. Returns =nil= (via =(values)=).
(values)) (values))
#+END_SRC #+END_SRC
*** Suspend backend (temporary)
Temporarily suspends the modern backend, restoring the terminal to a
usable state so the shell (or parent process) can take over. Called
before =SIGTSTP= or similar process suspension.
Shows the cursor and exits the alternate screen buffer so the user
sees the normal terminal content. Does NOT disable mouse modes or
kitty keyboard — those would add ~100ms of overhead on every
suspend/resume cycle and are harmless while suspended (the terminal
just ignores the escape sequences).
#+BEGIN_SRC lisp :tangle ../src/backend/modern.lisp
(defmethod suspend-backend ((b modern-backend))
(cursor-show b)
(backend-write b (format nil "~C[?1049l" #\Esc)) ; normal screen
(cursor-move b 0 0)
(finish-output (backend-output-stream b))
(values))
#+END_SRC
*** Resume backend (after suspend)
Re-initializes the modern backend after a suspension. Called after
=SIGCONT= or similar process resume.
Re-enters the alternate screen buffer and re-enables all input
features (mouse, bracketed paste, kitty keyboard). The application
is responsible for redrawing the full screen after resume.
#+BEGIN_SRC lisp :tangle ../src/backend/modern.lisp
(defmethod resume-backend ((b modern-backend))
(backend-write b (format nil "~C[?1049h" #\Esc)) ; alt screen
(backend-write b (format nil "~C[?1000h" #\Esc)) ; mouse basic
(backend-write b (format nil "~C[?1002h" #\Esc)) ; mouse drag
(backend-write b (format nil "~C[?1006h" #\Esc)) ; SGR mouse
(backend-write b (format nil "~C[?2004h" #\Esc)) ; bracketed paste
(backend-write b (format nil "~C[?u" #\Esc)) ; kitty keyboard
(cursor-hide b)
(finish-output (backend-output-stream b))
(values))
#+END_SRC
** Backend-size via ioctl ** Backend-size via ioctl
*** backend-size *** backend-size

View File

@@ -8,6 +8,16 @@
(defgeneric shutdown-backend (backend) (defgeneric shutdown-backend (backend)
(:method ((b backend)) (values))) (:method ((b backend)) (values)))
(defgeneric suspend-backend (backend)
(:documentation "Temporarily suspend the backend, restoring terminal to normal state.
Called before SIGTSTP or similar suspension. Application should redraw after resume.")
(:method ((b backend)) (values)))
(defgeneric resume-backend (backend)
(:documentation "Re-initialize the backend after suspension.
Called after SIGCONT or similar resume. Re-enables raw mode and backend features.")
(:method ((b backend)) (values)))
(defgeneric backend-size (backend) (defgeneric backend-size (backend)
(:method ((b backend)) (:method ((b backend))
(values 80 24))) (values 80 24)))

View File

@@ -72,7 +72,7 @@
(test osc8-escape (test osc8-escape
"OSC 8 hyperlink escape wraps text" "OSC 8 hyperlink escape wraps text"
(is (equal (cl-tty.backend::osc8-link "http://example.com" "click here") (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)))) #\Esc #\Esc #\Esc #\Esc))))
(test hex-color-parsing (test hex-color-parsing
@@ -101,10 +101,16 @@
(is (equal (cl-tty.backend::border-char :rounded :top-left) "╭")) (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 :horizontal) "─"))
(is (equal (cl-tty.backend::border-char :rounded :vertical) "│")) (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 (test border-char-double
"modern-border-char returns double-line chars" "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 :top-left) "╔"))
(is (equal (cl-tty.backend::border-char :double :horizontal) "═")) (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) "║")))
(test suspend-resume-noop
"suspend-backend and resume-backend are no-ops in test context"
(let ((b (make-modern-backend)))
(is (null (multiple-value-list (suspend-backend b))))
(is (null (multiple-value-list (resume-backend b))))))

View File

@@ -90,7 +90,7 @@ as a fallback when a keyword is not in *named-colors*.")
(defun osc8-link (url text) (defun osc8-link (url text)
"Wrap TEXT in an OSC 8 hyperlink to URL." "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)) #\Esc url #\Esc text #\Esc #\Esc))
(defparameter *border-chars* (defparameter *border-chars*
@@ -143,6 +143,24 @@ as a fallback when a keyword is not in *named-colors*.")
(finish-output (backend-output-stream b)) (finish-output (backend-output-stream b))
(values)) (values))
(defmethod suspend-backend ((b modern-backend))
(cursor-show b)
(backend-write b (format nil "~C[?1049l" #\Esc)) ; normal screen
(cursor-move b 0 0)
(finish-output (backend-output-stream b))
(values))
(defmethod resume-backend ((b modern-backend))
(backend-write b (format nil "~C[?1049h" #\Esc)) ; alt screen
(backend-write b (format nil "~C[?1000h" #\Esc)) ; mouse basic
(backend-write b (format nil "~C[?1002h" #\Esc)) ; mouse drag
(backend-write b (format nil "~C[?1006h" #\Esc)) ; SGR mouse
(backend-write b (format nil "~C[?2004h" #\Esc)) ; bracketed paste
(backend-write b (format nil "~C[?u" #\Esc)) ; kitty keyboard
(cursor-hide b)
(finish-output (backend-output-stream b))
(values))
(defmethod backend-size ((b modern-backend)) (defmethod backend-size ((b modern-backend))
(let* ((+tiocgwinsz+ 21523) ; 0x5413 on Linux (let* ((+tiocgwinsz+ 21523) ; 0x5413 on Linux
(winsize (sb-alien:make-alien sb-alien:unsigned-short 4))) (winsize (sb-alien:make-alien sb-alien:unsigned-short 4)))

View File

@@ -5,6 +5,7 @@
#:backend #:simple-backend #:backend #:simple-backend
;; Lifecycle ;; Lifecycle
#:initialize-backend #:shutdown-backend #:initialize-backend #:shutdown-backend
#:suspend-backend #:resume-backend
#:backend-size #:backend-write #:backend-clear #:backend-size #:backend-write #:backend-clear
;; Drawing ;; Drawing
#:draw-text #:draw-border #:draw-rect #:draw-text #:draw-border #:draw-rect

View File

@@ -15,6 +15,12 @@
(defmethod shutdown-backend ((b simple-backend)) (defmethod shutdown-backend ((b simple-backend))
(values)) (values))
(defmethod suspend-backend ((b simple-backend))
(values))
(defmethod resume-backend ((b simple-backend))
(values))
(defmethod backend-size ((b simple-backend)) (defmethod backend-size ((b simple-backend))
;; Try ioctl, fall back to 80x24 ;; Try ioctl, fall back to 80x24
(values 80 24)) (values 80 24))

View File

@@ -103,6 +103,8 @@
(is (null (multiple-value-list (cursor-style b :block)))) (is (null (multiple-value-list (cursor-style b :block))))
(is (null (multiple-value-list (begin-sync b)))) (is (null (multiple-value-list (begin-sync b))))
(is (null (multiple-value-list (end-sync b)))) (is (null (multiple-value-list (end-sync b))))
(is (null (multiple-value-list (suspend-backend b))))
(is (null (multiple-value-list (resume-backend b))))
(shutdown-backend b))) (shutdown-backend b)))
(test sync-is-noop-on-simple (test sync-is-noop-on-simple