v1.0.0 release
Bug fixes:
- Fix OSC8 format strings (backslash escape layering) in modern-backend.org
- Test format string had single backslash instead of double, causing
unclosed CL string that cascaded through 3 subsequent test forms
- Implementation format string had leading escaped quote (not a string
opener) and triple-backslash ending (also not a string terminator)
- Fix missing closing parens in border-char-rounded and border-char-double tests
- Fix ASDF input-tests pathname (file lives in tests/, not src/components/)
New features:
- Implement suspend-backend / resume-backend protocol methods
- modern-backend: exit/enter alt screen, re-enable mouse/kitty/bracketed-paste
- simple-backend: no-ops (no terminal state to preserve)
Infrastructure:
- Update test suite to cover suspend/resume (backend + modern-backend suites)
- 454 checks, 100% pass across 14 test suites
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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))))))
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user