aboutsummaryrefslogtreecommitdiff
path: root/tests/test-eat-config--xtwinops.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-13 16:01:51 -0500
committerCraig Jennings <c@cjennings.net>2026-07-13 16:01:51 -0500
commitc53671b06e585b5f663e0218490a855e7389d29e (patch)
tree07bc3778bb8f624fc6690cc9ae02248eca658d90 /tests/test-eat-config--xtwinops.el
parent56a3ed860b85ade4ccbcc42a4149fa9d83c34420 (diff)
downloaddotemacs-c53671b06e585b5f663e0218490a855e7389d29e.tar.gz
dotemacs-c53671b06e585b5f663e0218490a855e7389d29e.zip
feat(eat-config): answer XTWINOPS queries so tmux renders sixel in EAT
tmux 3.7b won't emit sixel until it learns the client's cell pixel size via CSI 14/16/18 t. EAT 0.9.4's parser has no CSI t clause and drops the queries, so images never render. I added a :before advice on eat--t-handle-output that scans the raw output and answers through the terminal's own input function (eat--t-with-env binds eat--t-term). I verified it live: images render and survive window switches, scrolling, and resizing. I guarded the advice-add so it installs only while EAT itself can't answer. Two answerers give tmux a double reply, and it forwards the second one's raw bytes into the pane as keystrokes. The guard keys on eat--t-send-window-size-report, the function an upstream clause would define. I kept an upstream-shaped parser-clause patch locally for a PR to akib/emacs-eat.
Diffstat (limited to 'tests/test-eat-config--xtwinops.el')
-rw-r--r--tests/test-eat-config--xtwinops.el120
1 files changed, 120 insertions, 0 deletions
diff --git a/tests/test-eat-config--xtwinops.el b/tests/test-eat-config--xtwinops.el
new file mode 100644
index 00000000..29f87f2f
--- /dev/null
+++ b/tests/test-eat-config--xtwinops.el
@@ -0,0 +1,120 @@
+;;; test-eat-config--xtwinops.el --- Tests for the EAT XTWINOPS window-size reply -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for the XTWINOPS (CSI <n> t) window-size responder. eat 0.9.4
+;; has no CSI <n> t handler, so it silently drops the window-size requests
+;; tmux 3.7b sends to learn the cell pixel size it needs before it will emit
+;; Sixel -- images then never render inside EAT. The module answers three
+;; requests: 14 (text area in pixels), 16 (cell size in pixels), 18 (text area
+;; in characters).
+;;
+;; Two pure pieces carry the logic and are tested here directly:
+;; - `cj/--eat-xtwinops-report' computes the reply string from the display and
+;; cell dimensions.
+;; - `cj/--eat-xtwinops-queries' extracts the request numbers from a chunk of
+;; terminal output.
+;; The thin accessor glue (`cj/--eat-send-window-size-report', which reads the
+;; live `eat--t-term' struct) is verified in the running daemon, since eat's
+;; structs are not loadable under `make test' (no package-initialize). The
+;; detector's dispatch is tested against a recording stub of the responder.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub keymap dep before loading the module (matches the other module tests).
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+(require 'eat-config)
+
+;;; --------------------------- reply computation ----------------------------
+
+(ert-deftest test-eat-config-xtwinops-report-normal-14-text-area-pixels ()
+ "Normal: request 14 reports text-area size in pixels (rows*ch by cols*cw)."
+ ;; 80x24 chars, 10x20 px cells -> height 24*20=480, width 80*10=800.
+ (should (equal (cj/--eat-xtwinops-report 14 80 24 10 20)
+ "\e[4;480;800t")))
+
+(ert-deftest test-eat-config-xtwinops-report-normal-16-cell-pixels ()
+ "Normal: request 16 reports the cell size in pixels (height then width)."
+ (should (equal (cj/--eat-xtwinops-report 16 80 24 10 20)
+ "\e[6;20;10t")))
+
+(ert-deftest test-eat-config-xtwinops-report-normal-18-text-area-chars ()
+ "Normal: request 18 reports the text-area size in characters (rows then cols)."
+ (should (equal (cj/--eat-xtwinops-report 18 80 24 10 20)
+ "\e[8;24;80t")))
+
+(ert-deftest test-eat-config-xtwinops-report-boundary-unit-cells ()
+ "Boundary: with eat's default 1x1 px cells, pixel dims equal the char dims."
+ (should (equal (cj/--eat-xtwinops-report 14 80 24 1 1) "\e[4;24;80t"))
+ (should (equal (cj/--eat-xtwinops-report 16 80 24 1 1) "\e[6;1;1t")))
+
+(ert-deftest test-eat-config-xtwinops-report-error-unknown-request-is-nil ()
+ "Error: any request number other than 14/16/18 returns nil (unanswered)."
+ (should (null (cj/--eat-xtwinops-report 15 80 24 10 20)))
+ (should (null (cj/--eat-xtwinops-report 24 80 24 10 20)))
+ (should (null (cj/--eat-xtwinops-report nil 80 24 10 20))))
+
+;;; ----------------------------- query detection ----------------------------
+
+(ert-deftest test-eat-config-xtwinops-queries-normal-single ()
+ "Normal: a lone CSI 14 t query is detected."
+ (should (equal (cj/--eat-xtwinops-queries "\e[14t") '(14))))
+
+(ert-deftest test-eat-config-xtwinops-queries-normal-embedded-multiple ()
+ "Normal: several queries embedded in other output are returned in order."
+ (should (equal (cj/--eat-xtwinops-queries "foo\e[14tbar\e[18tbaz\e[16t")
+ '(14 18 16))))
+
+(ert-deftest test-eat-config-xtwinops-queries-boundary-none ()
+ "Boundary: output with no XTWINOPS query returns nil, including empty."
+ (should (null (cj/--eat-xtwinops-queries "")))
+ (should (null (cj/--eat-xtwinops-queries "hello\e[0m\e[2J"))))
+
+(ert-deftest test-eat-config-xtwinops-queries-error-unhandled-ops-ignored ()
+ "Error: CSI t ops we do not answer (and parametrized forms) are not matched.
+`\\e[24t' is a resize op, `\\e[3;14t' carries a leading param -- neither is the
+bare CSI 14/16/18 t we answer, so both are ignored."
+ (should (null (cj/--eat-xtwinops-queries "\e[24t")))
+ (should (null (cj/--eat-xtwinops-queries "\e[3;14t")))
+ (should (null (cj/--eat-xtwinops-queries "\e[114t"))))
+
+;;; --------------------------- advice-needed guard --------------------------
+
+(ert-deftest test-eat-config-xtwinops-advice-needed-normal-eat-0-9-4 ()
+ "Normal: on eat 0.9.4 (no upstream CSI t clause) the advice is needed."
+ ;; The upstream parser clause defines `eat--t-send-window-size-report';
+ ;; 0.9.4 does not, so it must be unbound here.
+ (should-not (fboundp 'eat--t-send-window-size-report))
+ (should (cj/--eat-xtwinops-advice-needed-p)))
+
+(ert-deftest test-eat-config-xtwinops-advice-needed-boundary-upstream-ships ()
+ "Boundary: once upstream defines the parser clause, the advice must NOT
+install -- both would answer and tmux gets a double reply."
+ (cl-letf (((symbol-function 'eat--t-send-window-size-report) #'ignore))
+ (should-not (cj/--eat-xtwinops-advice-needed-p))))
+
+;;; ------------------------- detector dispatch (seam) -----------------------
+
+(ert-deftest test-eat-config-xtwinops-answer-dispatches-once-per-query ()
+ "Integration: the detector calls the responder once per query, in order.
+Mocks only our own responder seam (`cj/--eat-send-window-size-report'); the
+detector under test does the real query extraction."
+ (let ((calls '()))
+ (cl-letf (((symbol-function 'cj/--eat-send-window-size-report)
+ (lambda (n) (push n calls))))
+ (cj/--eat-answer-xtwinops "\e[14t\e[16t\e[18t"))
+ (should (equal (nreverse calls) '(14 16 18)))))
+
+(ert-deftest test-eat-config-xtwinops-answer-no-query-no-call ()
+ "Boundary: output with no query never calls the responder."
+ (let ((called nil))
+ (cl-letf (((symbol-function 'cj/--eat-send-window-size-report)
+ (lambda (_n) (setq called t))))
+ (cj/--eat-answer-xtwinops "no query here\e[2J"))
+ (should-not called)))
+
+(provide 'test-eat-config--xtwinops)
+;;; test-eat-config--xtwinops.el ends here