From edd5a7b8d1182fd65ea4c4a29736eb0de9e7ac1f Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 11 May 2026 22:41:34 +0000 Subject: [PATCH] v0.14.0: Mouse improvements - selection tracking and link clicking --- backend/classes.lisp | 3 +- org/framebuffer.org | 3 +- org/mouse.org | 32 ++++++++++++++++++ src/rendering/framebuffer.lisp | 3 +- tests/framebuffer-tests.lisp | 61 ++++++++++++++++++++++++++++++++++ tests/mouse-tests.lisp | 29 ++++++++++++++++ 6 files changed, 128 insertions(+), 3 deletions(-) diff --git a/backend/classes.lisp b/backend/classes.lisp index 2d97518..4c87c30 100644 --- a/backend/classes.lisp +++ b/backend/classes.lisp @@ -19,7 +19,8 @@ (backend-write b (format nil "~C[2J~C[H" #\Esc #\Esc)))) (defgeneric draw-text (backend x y string fg bg &key - bold italic underline reverse dim blink)) + bold italic underline reverse dim blink + &allow-other-keys)) (defgeneric draw-border (backend x y width height &key style fg bg title title-align)) diff --git a/org/framebuffer.org b/org/framebuffer.org index bb3e53d..cf4455e 100644 --- a/org/framebuffer.org +++ b/org/framebuffer.org @@ -127,7 +127,8 @@ See =docs/plans/2026-05-11-rendering-pipeline.md= for full implementation plan. #:make-framebuffer #:fb-framebuffer #:framebuffer-width #:framebuffer-height #:diff-framebuffers #:flush-framebuffer - #:with-scissor)) + #:with-scissor + #:extract-text #:fb-cell-link-url)) #+END_SRC #+BEGIN_SRC lisp :tangle ../src/rendering/framebuffer.lisp diff --git a/org/mouse.org b/org/mouse.org index 2c6ef60..df3bba6 100644 --- a/org/mouse.org +++ b/org/mouse.org @@ -102,3 +102,35 @@ module adds: (setf cl-tty.mouse::*selection* (make-selection :text "hello")) (is (equal "hello" (get-selection)))) #+END_SRC + +** Selection tracking + +#+BEGIN_SRC lisp :tangle ../tests/mouse-tests.lisp +(def-test start-selection-initializes-state () + (start-selection 5 10) + (is-true (selection-active-p)) + (is (equal '(5 . 10) cl-tty.mouse::*selection-start*)) + (is (equal '(5 . 10) cl-tty.mouse::*selection-end*)) + (setf cl-tty.mouse::*selection-active* nil + cl-tty.mouse::*selection-start* nil + cl-tty.mouse::*selection-end* nil)) + +(def-test update-selection-moves-end () + (start-selection 0 0) + (update-selection 3 7) + (is (equal '(3 . 7) cl-tty.mouse::*selection-end*)) + (setf cl-tty.mouse::*selection-active* nil + cl-tty.mouse::*selection-start* nil + cl-tty.mouse::*selection-end* nil)) + +(def-test finalize-selection-extracts-text () + (let* ((fb-be (cl-tty.rendering:make-framebuffer-backend)) + (fb (cl-tty.rendering:fb-framebuffer fb-be))) + (cl-tty.backend:draw-text fb-be 0 0 "hello" nil nil) + (cl-tty.backend:draw-text fb-be 0 1 "world" nil nil) + (start-selection 0 0) + (update-selection 4 1) + (let ((text (finalize-selection fb))) + (is (equal "hello +world" text))))) +#+END_SRC diff --git a/src/rendering/framebuffer.lisp b/src/rendering/framebuffer.lisp index 02ed9d5..67012d8 100644 --- a/src/rendering/framebuffer.lisp +++ b/src/rendering/framebuffer.lisp @@ -7,7 +7,8 @@ #:make-framebuffer #:fb-framebuffer #:framebuffer-width #:framebuffer-height #:diff-framebuffers #:flush-framebuffer - #:with-scissor)) + #:with-scissor + #:extract-text #:fb-cell-link-url)) (in-package :cl-tty.rendering) diff --git a/tests/framebuffer-tests.lisp b/tests/framebuffer-tests.lisp index 0f8c035..927619a 100644 --- a/tests/framebuffer-tests.lisp +++ b/tests/framebuffer-tests.lisp @@ -64,3 +64,64 @@ (draw-text fb 0 0 "X" :red nil) (let ((changed (flush-framebuffer (make-framebuffer 80 24) (fb-framebuffer fb) real-be))) (is (>= changed 1))))) + +;; ── Frame inspection ────────────────────────────────────────── + +(test fb-cell-link-url-returns-nil-for-blank-cell + (let ((fb (make-framebuffer 10 10))) + (is (null (fb-cell-link-url fb 5 5))))) + +(test fb-cell-link-url-finds-link-url + (let ((fb (make-framebuffer-backend))) + (draw-text fb 0 0 "click" nil nil :link-url "https://example.com") + (is (equal "https://example.com" (fb-cell-link-url (fb-framebuffer fb) 0 0))) + (is (null (fb-cell-link-url (fb-framebuffer fb) 5 5))))) + +(test fb-cell-link-url-out-of-bounds-returns-nil + (let ((fb (make-framebuffer 5 5))) + (is (null (fb-cell-link-url fb 10 10))))) + +(test extract-text-single-row + (let ((fb (make-framebuffer-backend))) + (draw-text fb 0 0 "hello" nil nil) + (let ((cells (fb-framebuffer fb))) + (is (equal "hello" (extract-text cells 0 0 4 0)))))) + +(test extract-text-multi-row + (let ((fb (make-framebuffer-backend))) + (draw-text fb 0 0 "abc" nil nil) + (draw-text fb 0 1 "def" nil nil) + (let* ((cells (fb-framebuffer fb)) + (text (extract-text cells 0 0 2 1))) + (is (equal "abc +def" text))))) + +;; --- Frame inspection ------------------------------------------------- +(test fb-cell-link-url-returns-nil-for-blank-cell + (let ((fb (make-framebuffer 10 10))) + (is (null (fb-cell-link-url fb 5 5))))) + +(test fb-cell-link-url-finds-link-url + (let ((fb (make-framebuffer-backend))) + (draw-text fb 0 0 "click" nil nil :link-url "https://example.com") + (is (equal "https://example.com" (fb-cell-link-url (fb-framebuffer fb) 0 0))) + (is (null (fb-cell-link-url (fb-framebuffer fb) 5 5))))) + +(test fb-cell-link-url-out-of-bounds-returns-nil + (let ((fb (make-framebuffer 5 5))) + (is (null (fb-cell-link-url fb 10 10))))) + +(test extract-text-single-row + (let ((fb (make-framebuffer-backend))) + (draw-text fb 0 0 "hello" nil nil) + (let ((cells (fb-framebuffer fb))) + (is (equal "hello" (extract-text cells 0 0 4 0)))))) + +(test extract-text-multi-row + (let ((fb (make-framebuffer-backend))) + (draw-text fb 0 0 "abc" nil nil) + (draw-text fb 0 1 "def" nil nil) + (let* ((cells (fb-framebuffer fb)) + (text (extract-text cells 0 0 2 1))) + (is (equal "abc +def" text))))) diff --git a/tests/mouse-tests.lisp b/tests/mouse-tests.lisp index dbfe8f0..8540c1f 100644 --- a/tests/mouse-tests.lisp +++ b/tests/mouse-tests.lisp @@ -15,3 +15,32 @@ (def-test selection-set-and-get () (setf cl-tty.mouse::*selection* (make-selection :text "hello")) (is (equal "hello" (get-selection)))) + +;; --- Selection tracking ------------------------------------------------- +(def-test start-selection-initializes-state () + (start-selection 5 10) + (is-true (selection-active-p)) + (is (equal '(5 . 10) cl-tty.mouse::*selection-start*)) + (is (equal '(5 . 10) cl-tty.mouse::*selection-end*)) + (setf cl-tty.mouse::*selection-active* nil + cl-tty.mouse::*selection-start* nil + cl-tty.mouse::*selection-end* nil)) + +(def-test update-selection-moves-end () + (start-selection 0 0) + (update-selection 3 7) + (is (equal '(3 . 7) cl-tty.mouse::*selection-end*)) + (setf cl-tty.mouse::*selection-active* nil + cl-tty.mouse::*selection-start* nil + cl-tty.mouse::*selection-end* nil)) + +(def-test finalize-selection-extracts-text () + (let* ((fb-be (cl-tty.rendering:make-framebuffer-backend)) + (fb (cl-tty.rendering:fb-framebuffer fb-be))) + (cl-tty.backend:draw-text fb-be 0 0 "hello" nil nil) + (cl-tty.backend:draw-text fb-be 0 1 "world" nil nil) + (start-selection 0 0) + (update-selection 4 1) + (let ((text (finalize-selection fb))) + (is (equal "hello +world" text)))))