aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/ai-term.el31
-rw-r--r--modules/cj-window-geometry-lib.el35
-rw-r--r--modules/cj-window-toggle-lib.el30
-rw-r--r--modules/term-config.el5
-rw-r--r--tests/test-ai-term--capture-state.el6
-rw-r--r--tests/test-cj-window-geometry-lib.el20
-rw-r--r--tests/test-cj-window-toggle-lib.el13
-rw-r--r--tests/test-term-toggle--display.el13
8 files changed, 93 insertions, 60 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el
index 6a1284aad..55728a3c4 100644
--- a/modules/ai-term.el
+++ b/modules/ai-term.el
@@ -445,21 +445,28 @@ the \"the displayed buffer changes\" bug. Falls back to the buffer-list
MRU when nil or when the remembered buffer has been killed.")
(defvar cj/--ai-term-last-size nil
- "Last user-chosen body size for the AI-term display.
+ "Last user-chosen size for the AI-term display.
Positive integer: body-columns when `cj/--ai-term-last-direction'
-is right or left, body-lines when below or above. nil means use
+is right or left, total-lines when below or above. nil means use
the host-aware default from `cj/--ai-term-default-size' (a float
-fraction).
-
-Body size, not total size, because total-width includes the
-right-edge divider when the window has a right sibling but excludes
-it when the window is at the frame edge. Capturing total-width
-from a rightmost agent (no divider) and replaying into a middle
-position (with divider) leaves the body 1 column short -- visible
-as 1 col of the sibling buffer peeking through where agent should
-have ended. Body-width is divider-independent and matches what the
-user actually sees.
+fraction). See `cj/window-replay-size' for the per-axis capture.
+
+The axis choice is asymmetric. Width captures body-width, not
+total-width: total-width includes the right-edge divider when the
+window has a right sibling but excludes it at the frame edge, so
+capturing total-width from a rightmost agent (no divider) and
+replaying into a middle position (with divider) leaves the body 1
+column short. Body-width is divider-independent.
+
+Height captures total-height, not body-height: every window has
+exactly one mode line regardless of position, so total-height has
+no divider-position problem, and total-height is the same whether
+the window is active or inactive. Body-height would subtract the
+mode line's pixel height, which differs between an active and an
+inactive (theme-shrunk) mode line -- capturing body-height active
+and replaying it inactive then re-measuring active drifts the
+window down by ~1 line per toggle (the F9 shrink bug, 2026-06-20).
Absolute values rather than fractions because
`display-buffer-in-direction' interprets a float `window-width' /
diff --git a/modules/cj-window-geometry-lib.el b/modules/cj-window-geometry-lib.el
index 4c0662124..4484a1d15 100644
--- a/modules/cj-window-geometry-lib.el
+++ b/modules/cj-window-geometry-lib.el
@@ -42,21 +42,34 @@ fails to span the full height."
((not spans-full-height) (if (= top root-top) 'above 'below))
(t (or default 'right)))))
-(defun cj/window-body-size (window direction)
- "Return WINDOW's body size on the axis matching DIRECTION.
+(defun cj/window-replay-size (window direction)
+ "Return WINDOW's size to capture for geometry replay, on DIRECTION's axis.
Returns body-width (columns) when DIRECTION is right or left.
-Returns body-height (lines) when DIRECTION is below or above.
-
-Body size, not total size, is the right thing to capture for
-geometry replay: total-width includes the right-side divider when
-the window has a right sibling but excludes it at the frame edge,
-so a captured rightmost window replayed into a middle position
-would leave the body 1 col short. Body size is divider-
-independent and matches what the user actually sees."
+Returns total-height (lines) when DIRECTION is below or above.
+
+The axis choice is deliberately asymmetric, for two different reasons:
+
+- Width: body-width, not total-width. Total-width includes the right-side
+ divider when the window has a right sibling but excludes it at the frame
+ edge, so a captured rightmost window replayed into a middle position would
+ leave the body 1 col short. Body-width is divider-independent and matches
+ what the user sees.
+
+- Height: total-height, not body-height. Every window carries exactly one
+ mode line regardless of position, so total-height has no analog of the
+ divider-position problem -- it is position-independent. Body-height does
+ NOT work here: it subtracts the mode line's *pixel* height, which differs
+ between an active (full-height) and an inactive (theme-shrunk) mode line.
+ Capturing body-height while the window is active and replaying it while the
+ window is displayed inactive then re-measuring active drifts the value down
+ by ~1 line per toggle whenever the inactive mode line is shorter than a text
+ line (e.g. a theme that sets `mode-line-inactive' to a sub-line height).
+ Total-height is identical active or inactive, so the capture/replay
+ round-trip is a fixed point."
(if (memq direction '(right left))
(window-body-width window)
- (window-body-height window)))
+ (window-total-height window)))
(defun cj/cardinal-to-edge-direction (direction)
"Map cardinal DIRECTION to its `display-buffer-in-direction' edge variant.
diff --git a/modules/cj-window-toggle-lib.el b/modules/cj-window-toggle-lib.el
index ba91f5a40..175a1d958 100644
--- a/modules/cj-window-toggle-lib.el
+++ b/modules/cj-window-toggle-lib.el
@@ -44,7 +44,7 @@ No-op when WINDOW is nil or not live."
(if (or (null allowed) (memq dir allowed))
(progn
(set direction-var dir)
- (set size-var (cj/window-body-size window dir)))
+ (set size-var (cj/window-replay-size window dir)))
(set direction-var default-direction)
(set size-var nil)))))
@@ -59,10 +59,12 @@ DEFAULT-SIZE when the stored values are nil. The cardinal direction
is mapped to its frame-edge variant via
`cj/cardinal-to-edge-direction' so the new buffer always lands at
the same frame edge regardless of the selected window. An integer
-size is wrapped in a `(body-columns . N)' / `(body-lines . N)' cons
-so `display-buffer-in-direction' sets the body explicitly,
-divider-independent. A float size passes through as a fraction of
-the new window's parent.
+size is wrapped per axis: a width size as a `(body-columns . N)'
+cons (divider-independent body width), a height size as a plain
+integer total-line count. Height uses total rather than body so the
+capture/replay round-trip is immune to the mode line's pixel height
+(see `cj/window-replay-size'). A float size passes through as a
+fraction of the new window's parent.
Caller-supplied ALIST entries for direction, window-width, or
window-height are stripped before delegating to
@@ -74,15 +76,15 @@ placement; the remaining alist entries are passed through."
(edge-direction (or (cj/cardinal-to-edge-direction direction)
(cj/cardinal-to-edge-direction default-direction)))
(size (or stored-size default-size))
- (size-key (if (memq direction '(right left))
- 'window-width
- 'window-height))
- (body-tag (if (memq direction '(right left))
- 'body-columns
- 'body-lines))
- (size-value (if (integerp size)
- (cons body-tag size)
- size))
+ (width-axis (memq direction '(right left)))
+ (size-key (if width-axis 'window-width 'window-height))
+ ;; A width integer is a body-column count (divider-independent); a
+ ;; height integer is a plain total-line count (mode-line-pixel-
+ ;; independent -- see `cj/window-replay-size'). Floats pass through.
+ (size-value (cond
+ ((not (integerp size)) size)
+ (width-axis (cons 'body-columns size))
+ (t size)))
(filtered (cl-remove-if
(lambda (cell)
(memq (car-safe cell)
diff --git a/modules/term-config.el b/modules/term-config.el
index 33f54d75a..0a7991409 100644
--- a/modules/term-config.el
+++ b/modules/term-config.el
@@ -313,8 +313,9 @@ Symbol: right, left, or below. `above' is never stored. nil means use the
default `below' for F12's traditional bottom split.")
(defvar cj/--term-toggle-last-size nil
- "Last user-chosen body size for the F12 terminal display.
-Positive integer: body-cols (right/left) or body-lines (below/above).
+ "Last user-chosen size for the F12 terminal display.
+Positive integer: body-cols (right/left) or total-lines (below/above) -- see
+`cj/window-replay-size' for why the vertical axis uses total, not body.
nil means fall back to `cj/term-toggle-window-height' as a fraction.")
(defun cj/--term-toggle-buffer-p (buffer)
diff --git a/tests/test-ai-term--capture-state.el b/tests/test-ai-term--capture-state.el
index 543f83ad7..aa7421350 100644
--- a/tests/test-ai-term--capture-state.el
+++ b/tests/test-ai-term--capture-state.el
@@ -27,7 +27,9 @@
(should (= cj/--ai-term-last-size (window-body-width right))))))
(ert-deftest test-ai-term--capture-state-below-split-sets-direction ()
- "Normal: below-split window -> direction=below, integer body-lines matching window."
+ "Normal: below-split window -> direction=below, integer total-lines matching window.
+The vertical axis captures total-height (not body-height) so the toggle
+round-trip is immune to the mode line's pixel height."
(save-window-excursion
(delete-other-windows)
(let ((below (split-window (selected-window) nil 'below))
@@ -36,7 +38,7 @@
(cj/--ai-term-capture-state below)
(should (eq cj/--ai-term-last-direction 'below))
(should (integerp cj/--ai-term-last-size))
- (should (= cj/--ai-term-last-size (window-body-height below))))))
+ (should (= cj/--ai-term-last-size (window-total-height below))))))
(ert-deftest test-ai-term--capture-state-noop-on-dead-window ()
"Boundary: nil window -> state remains unchanged."
diff --git a/tests/test-cj-window-geometry-lib.el b/tests/test-cj-window-geometry-lib.el
index 938749f21..d32a48a92 100644
--- a/tests/test-cj-window-geometry-lib.el
+++ b/tests/test-cj-window-geometry-lib.el
@@ -2,7 +2,7 @@
;;; Commentary:
;; Tests the pure helpers in `cj-window-geometry-lib.el':
-;; `cj/window-direction', `cj/window-body-size',
+;; `cj/window-direction', `cj/window-replay-size',
;; `cj/cardinal-to-edge-direction', and `cj/window-at-edge'.
;;; Code:
@@ -52,30 +52,32 @@
(delete-other-windows)
(should (eq (cj/window-direction (selected-window) 'below) 'below))))
-(ert-deftest test-cj-window-geometry--body-size-right-returns-body-cols ()
+(ert-deftest test-cj-window-geometry--replay-size-right-returns-body-cols ()
"Normal: right window with direction='right -> body-width in cols."
(save-window-excursion
(delete-other-windows)
(let ((right (split-window (selected-window) nil 'right)))
- (should (= (cj/window-body-size right 'right)
+ (should (= (cj/window-replay-size right 'right)
(window-body-width right))))))
-(ert-deftest test-cj-window-geometry--body-size-below-returns-body-lines ()
- "Normal: below window with direction='below -> body-height in lines."
+(ert-deftest test-cj-window-geometry--replay-size-below-returns-total-lines ()
+ "Normal: below window with direction='below -> total-height in lines.
+The vertical axis captures total-height (not body-height) so the capture/
+replay round-trip is immune to the mode line's pixel height."
(save-window-excursion
(delete-other-windows)
(let ((below (split-window (selected-window) nil 'below)))
- (should (= (cj/window-body-size below 'below)
- (window-body-height below))))))
+ (should (= (cj/window-replay-size below 'below)
+ (window-total-height below))))))
-(ert-deftest test-cj-window-geometry--body-size-narrow-window ()
+(ert-deftest test-cj-window-geometry--replay-size-narrow-window ()
"Normal: deliberately narrow right window -> matching body cols."
(save-window-excursion
(delete-other-windows)
(let* ((frame-w (frame-width))
(target-cols (/ frame-w 4))
(right (split-window (selected-window) (- target-cols) 'right)))
- (should (= (cj/window-body-size right 'right)
+ (should (= (cj/window-replay-size right 'right)
(window-body-width right))))))
(ert-deftest test-cj-window-geometry--cardinal-to-edge-right ()
diff --git a/tests/test-cj-window-toggle-lib.el b/tests/test-cj-window-toggle-lib.el
index 0762e255c..5edd06e96 100644
--- a/tests/test-cj-window-toggle-lib.el
+++ b/tests/test-cj-window-toggle-lib.el
@@ -36,7 +36,9 @@
(window-body-width right))))))
(ert-deftest test-cj-window-toggle-capture-records-below-split ()
- "Normal: below-split window writes direction=below and integer body-lines."
+ "Normal: below-split window writes direction=below and integer total-lines.
+The vertical axis captures total-height, not body-height, so the round-trip
+is immune to the mode line's pixel height (see `cj/window-replay-size')."
(save-window-excursion
(delete-other-windows)
(let ((below (split-window (selected-window) nil 'below))
@@ -49,7 +51,7 @@
(should (eq test-cj-window-toggle--last-direction 'below))
(should (integerp test-cj-window-toggle--last-size))
(should (= test-cj-window-toggle--last-size
- (window-body-height below))))))
+ (window-total-height below))))))
(ert-deftest test-cj-window-toggle-capture-falls-back-to-default-direction ()
"Boundary: window filling the frame uses the supplied default direction."
@@ -156,7 +158,9 @@ transfer; clearing it lets the consumer's default size apply."
(should (eq (cdr (assq 'inhibit-same-window received-alist)) t))))
(ert-deftest test-cj-window-toggle-display-saved-maps-below-to-bottom ()
- "Normal: saved below + integer size -> bottom edge, body-lines cons."
+ "Normal: saved below + integer size -> bottom edge, plain total-line count.
+The height axis replays a total-line integer (not a body-lines cons) so the
+round-trip is immune to the mode line's pixel height."
(let (received-alist
(test-cj-window-toggle--last-direction 'below)
(test-cj-window-toggle--last-size 12))
@@ -169,8 +173,7 @@ transfer; clearing it lets the consumer's default size apply."
'test-cj-window-toggle--last-size
0.7))
(should (eq (cdr (assq 'direction received-alist)) 'bottom))
- (should (equal (cdr (assq 'window-height received-alist))
- '(body-lines . 12)))
+ (should (equal (cdr (assq 'window-height received-alist)) 12))
(should-not (assq 'window-width received-alist))))
(ert-deftest test-cj-window-toggle-display-saved-maps-right-to-rightmost ()
diff --git a/tests/test-term-toggle--display.el b/tests/test-term-toggle--display.el
index 7fa7f0a98..d6dd33da2 100644
--- a/tests/test-term-toggle--display.el
+++ b/tests/test-term-toggle--display.el
@@ -17,7 +17,9 @@
(require 'term-config)
(ert-deftest test-term-toggle--capture-state-records-direction-and-size ()
- "Normal: capture-state writes direction and integer body size."
+ "Normal: capture-state writes direction and integer size.
+The vertical axis captures total-height (not body-height) so the toggle
+round-trip is immune to the mode line's pixel height."
(save-window-excursion
(delete-other-windows)
(let ((below (split-window (selected-window) nil 'below))
@@ -26,7 +28,7 @@
(cj/--term-toggle-capture-state below)
(should (eq cj/--term-toggle-last-direction 'below))
(should (integerp cj/--term-toggle-last-size))
- (should (= cj/--term-toggle-last-size (window-body-height below))))))
+ (should (= cj/--term-toggle-last-size (window-total-height below))))))
(ert-deftest test-term-toggle--capture-state-noop-on-dead-window ()
"Boundary: nil window -> state remains unchanged."
@@ -50,7 +52,9 @@
(should (eq (cdr (assq 'inhibit-same-window received-alist)) t))))
(ert-deftest test-term-toggle--display-saved-maps-cardinal-to-edge ()
- "Normal: saved 'below maps to bottom edge; integer size wraps in body-lines."
+ "Normal: saved 'below maps to bottom edge; integer size is a plain total-line count.
+The height axis replays a total-line integer (not a body-lines cons) so the
+round-trip is immune to the mode line's pixel height."
(let (received-alist
(cj/--term-toggle-last-direction 'below)
(cj/--term-toggle-last-size 12))
@@ -58,8 +62,7 @@
(lambda (_b a) (setq received-alist a) 'fake-window)))
(cj/--term-toggle-display-saved 'fake-buf nil))
(should (eq (cdr (assq 'direction received-alist)) 'bottom))
- (should (equal (cdr (assq 'window-height received-alist))
- '(body-lines . 12)))
+ (should (equal (cdr (assq 'window-height received-alist)) 12))
(should-not (assq 'window-width received-alist))))
(ert-deftest test-term-toggle--display-saved-strips-conflicting-alist-entries ()