aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/ai-vterm.el57
-rw-r--r--modules/cj-window-geometry.el81
-rw-r--r--modules/eshell-vterm-config.el50
-rw-r--r--tests/test-ai-vterm--window-geometry.el88
-rw-r--r--tests/test-cj-window-geometry.el103
-rw-r--r--tests/test-vterm-toggle--display.el29
6 files changed, 201 insertions, 207 deletions
diff --git a/modules/ai-vterm.el b/modules/ai-vterm.el
index 9b47e33d..e817f441 100644
--- a/modules/ai-vterm.el
+++ b/modules/ai-vterm.el
@@ -36,6 +36,7 @@
(require 'cl-lib)
(require 'seq)
+(require 'cj-window-geometry)
(declare-function vterm "vterm" (&optional buffer-name))
(declare-function vterm-send-string "vterm" (string &optional paste-p))
@@ -228,57 +229,19 @@ and a fraction-of-frame produces the wrong size on replay
(squeezes the other windows). An integer is unambiguous, at the
cost of not auto-scaling if the frame itself resizes.")
-(defun cj/--ai-vterm-window-direction (window)
- "Return the side WINDOW occupies in its frame.
-
-Returns one of right, below, left, above. Falls back to right when
-WINDOW fills its frame's root area (single-window or atypical
-layout), since right is the module's default split direction.
-
-Comparison uses `frame-root-window' edges rather than frame edges so
-the minibuffer doesn't make every full-area window look like it
-fails to span the full height."
- (let* ((root (frame-root-window (window-frame window)))
- (edges (window-edges window))
- (root-edges (window-edges root))
- (left (nth 0 edges))
- (top (nth 1 edges))
- (right (nth 2 edges))
- (bottom (nth 3 edges))
- (root-left (nth 0 root-edges))
- (root-top (nth 1 root-edges))
- (root-right (nth 2 root-edges))
- (root-bottom (nth 3 root-edges))
- (spans-full-width (and (= left root-left) (= right root-right)))
- (spans-full-height (and (= top root-top) (= bottom root-bottom))))
- (cond
- ((not spans-full-width) (if (= left root-left) 'left 'right))
- ((not spans-full-height) (if (= top root-top) 'above 'below))
- (t 'right))))
-
-(defun cj/--ai-vterm-window-size (window direction)
- "Return WINDOW's body size in cols (right/left) or lines (below/above).
-
-Returns body width or body height -- the count of characters
-visible in the text content area, independent of fringes,
-scrollbars, or window dividers. See `cj/--ai-vterm-last-size' for
-why body size, not total size, is the right thing to capture."
- (if (memq direction '(right left))
- (window-body-width window)
- (window-body-height window)))
-
(defun cj/--ai-vterm-capture-state (window)
"Capture WINDOW's direction and size into module-level state.
Sets `cj/--ai-vterm-last-direction' and `cj/--ai-vterm-last-size'
so a subsequent F9 display can restore the user's chosen orientation
-and size. Called at toggle-off (just before `quit-window' tears the
-window down).
+and size. Called at toggle-off (just before the window is torn
+down). The default direction is 'right -- the module's side-panel
+default.
Does nothing when WINDOW is not live."
(when (window-live-p window)
- (let* ((dir (cj/--ai-vterm-window-direction window))
- (size (cj/--ai-vterm-window-size window dir)))
+ (let* ((dir (cj/window-direction window 'right))
+ (size (cj/window-body-size window dir)))
(setq cj/--ai-vterm-last-direction dir
cj/--ai-vterm-last-size size))))
@@ -333,12 +296,8 @@ stripped so the saved-state values control placement -- callers
shouldn't specify direction or size in the rule when this action is
used."
(let* ((direction (or cj/--ai-vterm-last-direction 'right))
- (edge-direction (pcase direction
- ('right 'rightmost)
- ('left 'leftmost)
- ('below 'bottom)
- ('above 'top)
- (_ 'rightmost)))
+ (edge-direction (or (cj/cardinal-to-edge-direction direction)
+ 'rightmost))
(size (or cj/--ai-vterm-last-size cj/ai-vterm-window-width))
(size-key (if (memq direction '(right left))
'window-width
diff --git a/modules/cj-window-geometry.el b/modules/cj-window-geometry.el
new file mode 100644
index 00000000..88fa83d4
--- /dev/null
+++ b/modules/cj-window-geometry.el
@@ -0,0 +1,81 @@
+;;; cj-window-geometry.el --- Pure window-geometry helpers -*- lexical-binding: t; -*-
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; Pure helpers for classifying a window's position in its frame and
+;; computing body sizes. Shared between `ai-vterm.el' (F9 dispatch)
+;; and `eshell-vterm-config.el' (F12 dispatch); the geometry-
+;; preservation pattern in both modules captures direction + body
+;; size at toggle-off and replays them on the next toggle-on.
+;;
+;; All functions are pure: they read window/frame edges and return
+;; classifications. No side effects, no state. Consumers wrap them
+;; with consumer-specific state vars and display logic.
+
+;;; Code:
+
+(defun cj/window-direction (window &optional default)
+ "Return the side WINDOW occupies in its frame.
+
+Returns one of right, below, left, above. Falls back to DEFAULT
+(or right when DEFAULT is nil) when WINDOW fills its frame's
+root area. Comparison uses `frame-root-window' edges so the
+minibuffer doesn't make every full-area window look like it
+fails to span the full height."
+ (let* ((root (frame-root-window (window-frame window)))
+ (edges (window-edges window))
+ (root-edges (window-edges root))
+ (left (nth 0 edges))
+ (top (nth 1 edges))
+ (right (nth 2 edges))
+ (bottom (nth 3 edges))
+ (root-left (nth 0 root-edges))
+ (root-top (nth 1 root-edges))
+ (root-right (nth 2 root-edges))
+ (root-bottom (nth 3 root-edges))
+ (spans-full-width (and (= left root-left) (= right root-right)))
+ (spans-full-height (and (= top root-top) (= bottom root-bottom))))
+ (cond
+ ((not spans-full-width) (if (= left root-left) 'left 'right))
+ ((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.
+
+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."
+ (if (memq direction '(right left))
+ (window-body-width window)
+ (window-body-height window)))
+
+(defun cj/cardinal-to-edge-direction (direction)
+ "Map cardinal DIRECTION to its `display-buffer-in-direction' edge variant.
+
+Returns rightmost/leftmost/bottom/top for right/left/below/above
+respectively. Returns nil for any other input.
+
+The edge variants route splits relative to the frame's main
+window rather than the selected window, so a toggle-on lands at
+the same frame edge regardless of which window is selected. The
+cardinal variants would split the selected window's tree branch
+instead, putting the new window mid-frame in multi-window
+layouts."
+ (pcase direction
+ ('right 'rightmost)
+ ('left 'leftmost)
+ ('below 'bottom)
+ ('above 'top)
+ (_ nil)))
+
+(provide 'cj-window-geometry)
+;;; cj-window-geometry.el ends here
diff --git a/modules/eshell-vterm-config.el b/modules/eshell-vterm-config.el
index ed257893..34738fa1 100644
--- a/modules/eshell-vterm-config.el
+++ b/modules/eshell-vterm-config.el
@@ -261,6 +261,7 @@ ai-vterm.el is loaded."
(require 'cl-lib)
(require 'seq)
+(require 'cj-window-geometry)
(defcustom cj/vterm-toggle-window-height 0.7
"Default fraction of frame height for the F12 vterm window.
@@ -306,43 +307,14 @@ FRAME defaults to the selected frame. Minibuffer is excluded."
(cj/--vterm-toggle-buffer-p (window-buffer w)))
(window-list (or frame (selected-frame)) 'never)))
-(defun cj/--vterm-toggle-window-direction (window)
- "Return the side WINDOW occupies in its frame.
-
-Returns one of right, below, left, above. Falls back to 'below
-(F12's traditional bottom split) when WINDOW fills its frame's
-root area. Comparison uses `frame-root-window' edges so the
-minibuffer doesn't make every full-area window look like it
-fails to span the full height."
- (let* ((root (frame-root-window (window-frame window)))
- (edges (window-edges window))
- (root-edges (window-edges root))
- (left (nth 0 edges))
- (top (nth 1 edges))
- (right (nth 2 edges))
- (bottom (nth 3 edges))
- (root-left (nth 0 root-edges))
- (root-top (nth 1 root-edges))
- (root-right (nth 2 root-edges))
- (root-bottom (nth 3 root-edges))
- (spans-full-width (and (= left root-left) (= right root-right)))
- (spans-full-height (and (= top root-top) (= bottom root-bottom))))
- (cond
- ((not spans-full-width) (if (= left root-left) 'left 'right))
- ((not spans-full-height) (if (= top root-top) 'above 'below))
- (t 'below))))
-
-(defun cj/--vterm-toggle-window-size (window direction)
- "Return WINDOW's body size in cols (right/left) or lines (below/above)."
- (if (memq direction '(right left))
- (window-body-width window)
- (window-body-height window)))
-
(defun cj/--vterm-toggle-capture-state (window)
- "Capture WINDOW's direction + body size into module-level state."
+ "Capture WINDOW's direction + body size into module-level state.
+
+Default direction is 'below to match F12's traditional bottom
+split when WINDOW fills the frame's root area."
(when (window-live-p window)
- (let* ((dir (cj/--vterm-toggle-window-direction window))
- (size (cj/--vterm-toggle-window-size window dir)))
+ (let* ((dir (cj/window-direction window 'below))
+ (size (cj/window-body-size window dir)))
(setq cj/--vterm-toggle-last-direction dir
cj/--vterm-toggle-last-size size))))
@@ -359,12 +331,8 @@ in a `(body-columns . N)' / `(body-lines . N)' cons so the body
width or height is set explicitly, divider-independent. A float
size passes through as a fraction of the new window's parent."
(let* ((direction (or cj/--vterm-toggle-last-direction 'below))
- (edge-direction (pcase direction
- ('right 'rightmost)
- ('left 'leftmost)
- ('below 'bottom)
- ('above 'top)
- (_ 'bottom)))
+ (edge-direction (or (cj/cardinal-to-edge-direction direction)
+ 'bottom))
(size (or cj/--vterm-toggle-last-size cj/vterm-toggle-window-height))
(size-key (if (memq direction '(right left))
'window-width
diff --git a/tests/test-ai-vterm--window-geometry.el b/tests/test-ai-vterm--window-geometry.el
deleted file mode 100644
index c2200273..00000000
--- a/tests/test-ai-vterm--window-geometry.el
+++ /dev/null
@@ -1,88 +0,0 @@
-;;; test-ai-vterm--window-geometry.el --- Tests for direction + fraction helpers -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Two pure helpers used by F9's geometry-preservation feature:
-;;
-;; - `cj/--ai-vterm-window-direction' classifies a window's position
-;; relative to its frame as right / below / left / above (with a
-;; right fallback when the window fills the frame).
-;;
-;; - `cj/--ai-vterm-window-fraction' returns the window's size on
-;; the matching axis as a fraction of the frame.
-;;
-;; Tests use real window splits in `save-window-excursion' rather
-;; than mocking, since the helpers consume `window-edges' and
-;; `frame-width' / `frame-height' directly.
-
-;;; Code:
-
-(require 'ert)
-
-(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
-(require 'ai-vterm)
-
-(ert-deftest test-ai-vterm--window-direction-right-split ()
- "Normal: 2-window vertical split, right-side window -> right."
- (save-window-excursion
- (delete-other-windows)
- (let ((right (split-window (selected-window) nil 'right)))
- (should (eq (cj/--ai-vterm-window-direction right) 'right)))))
-
-(ert-deftest test-ai-vterm--window-direction-left-split ()
- "Normal: 2-window vertical split, left-side window -> left."
- (save-window-excursion
- (delete-other-windows)
- (split-window (selected-window) nil 'right)
- (should (eq (cj/--ai-vterm-window-direction (selected-window)) 'left))))
-
-(ert-deftest test-ai-vterm--window-direction-below-split ()
- "Normal: 2-window horizontal split, bottom window -> below."
- (save-window-excursion
- (delete-other-windows)
- (let ((below (split-window (selected-window) nil 'below)))
- (should (eq (cj/--ai-vterm-window-direction below) 'below)))))
-
-(ert-deftest test-ai-vterm--window-direction-above-split ()
- "Normal: 2-window horizontal split, top window -> above."
- (save-window-excursion
- (delete-other-windows)
- (split-window (selected-window) nil 'below)
- (should (eq (cj/--ai-vterm-window-direction (selected-window)) 'above))))
-
-(ert-deftest test-ai-vterm--window-direction-single-window-fallback ()
- "Boundary: single-window frame -> default right."
- (save-window-excursion
- (delete-other-windows)
- (should (eq (cj/--ai-vterm-window-direction (selected-window)) 'right))))
-
-(ert-deftest test-ai-vterm--window-size-right-split-returns-body-cols ()
- "Normal: right window -> integer body-cols matching window-body-width."
- (save-window-excursion
- (delete-other-windows)
- (let* ((right (split-window (selected-window) nil 'right))
- (size (cj/--ai-vterm-window-size right 'right)))
- (should (integerp size))
- (should (= size (window-body-width right))))))
-
-(ert-deftest test-ai-vterm--window-size-below-split-returns-body-lines ()
- "Normal: bottom window -> integer body-lines matching window-body-height."
- (save-window-excursion
- (delete-other-windows)
- (let* ((below (split-window (selected-window) nil 'below))
- (size (cj/--ai-vterm-window-size below 'below)))
- (should (integerp size))
- (should (= size (window-body-height below))))))
-
-(ert-deftest test-ai-vterm--window-size-narrow-right-split ()
- "Normal: deliberately narrow right window -> matching body-col count."
- (save-window-excursion
- (delete-other-windows)
- (let* ((frame-w (frame-width))
- (target-cols (/ frame-w 4))
- (right (split-window (selected-window) (- target-cols) 'right))
- (size (cj/--ai-vterm-window-size right 'right)))
- (should (integerp size))
- (should (= size (window-body-width right))))))
-
-(provide 'test-ai-vterm--window-geometry)
-;;; test-ai-vterm--window-geometry.el ends here
diff --git a/tests/test-cj-window-geometry.el b/tests/test-cj-window-geometry.el
new file mode 100644
index 00000000..8b0b9732
--- /dev/null
+++ b/tests/test-cj-window-geometry.el
@@ -0,0 +1,103 @@
+;;; test-cj-window-geometry.el --- Tests for the shared window-geometry helpers -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests the three pure helpers in `cj-window-geometry.el':
+;; `cj/window-direction', `cj/window-body-size', and
+;; `cj/cardinal-to-edge-direction'.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'cj-window-geometry)
+
+(ert-deftest test-cj-window-geometry--direction-right-split ()
+ "Normal: 2-window vertical split, right-side window -> right."
+ (save-window-excursion
+ (delete-other-windows)
+ (let ((right (split-window (selected-window) nil 'right)))
+ (should (eq (cj/window-direction right) 'right)))))
+
+(ert-deftest test-cj-window-geometry--direction-left-split ()
+ "Normal: 2-window vertical split, left-side window -> left."
+ (save-window-excursion
+ (delete-other-windows)
+ (split-window (selected-window) nil 'right)
+ (should (eq (cj/window-direction (selected-window)) 'left))))
+
+(ert-deftest test-cj-window-geometry--direction-below-split ()
+ "Normal: 2-window horizontal split, bottom window -> below."
+ (save-window-excursion
+ (delete-other-windows)
+ (let ((below (split-window (selected-window) nil 'below)))
+ (should (eq (cj/window-direction below) 'below)))))
+
+(ert-deftest test-cj-window-geometry--direction-above-split ()
+ "Normal: 2-window horizontal split, top window -> above."
+ (save-window-excursion
+ (delete-other-windows)
+ (split-window (selected-window) nil 'below)
+ (should (eq (cj/window-direction (selected-window)) 'above))))
+
+(ert-deftest test-cj-window-geometry--direction-single-window-default-right ()
+ "Boundary: single-window frame, no DEFAULT arg -> 'right."
+ (save-window-excursion
+ (delete-other-windows)
+ (should (eq (cj/window-direction (selected-window)) 'right))))
+
+(ert-deftest test-cj-window-geometry--direction-single-window-with-default ()
+ "Boundary: single-window frame with DEFAULT='below -> 'below."
+ (save-window-excursion
+ (delete-other-windows)
+ (should (eq (cj/window-direction (selected-window) 'below) 'below))))
+
+(ert-deftest test-cj-window-geometry--body-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)
+ (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."
+ (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))))))
+
+(ert-deftest test-cj-window-geometry--body-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)
+ (window-body-width right))))))
+
+(ert-deftest test-cj-window-geometry--cardinal-to-edge-right ()
+ "Normal: 'right -> 'rightmost."
+ (should (eq (cj/cardinal-to-edge-direction 'right) 'rightmost)))
+
+(ert-deftest test-cj-window-geometry--cardinal-to-edge-left ()
+ "Normal: 'left -> 'leftmost."
+ (should (eq (cj/cardinal-to-edge-direction 'left) 'leftmost)))
+
+(ert-deftest test-cj-window-geometry--cardinal-to-edge-below ()
+ "Normal: 'below -> 'bottom."
+ (should (eq (cj/cardinal-to-edge-direction 'below) 'bottom)))
+
+(ert-deftest test-cj-window-geometry--cardinal-to-edge-above ()
+ "Normal: 'above -> 'top."
+ (should (eq (cj/cardinal-to-edge-direction 'above) 'top)))
+
+(ert-deftest test-cj-window-geometry--cardinal-to-edge-unknown-returns-nil ()
+ "Boundary: an unknown direction symbol -> nil."
+ (should (null (cj/cardinal-to-edge-direction 'sideways)))
+ (should (null (cj/cardinal-to-edge-direction nil))))
+
+(provide 'test-cj-window-geometry)
+;;; test-cj-window-geometry.el ends here
diff --git a/tests/test-vterm-toggle--display.el b/tests/test-vterm-toggle--display.el
index 70fa61ac..ed7cd858 100644
--- a/tests/test-vterm-toggle--display.el
+++ b/tests/test-vterm-toggle--display.el
@@ -16,35 +16,6 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'eshell-vterm-config)
-(ert-deftest test-vterm-toggle--window-direction-single-window-defaults-to-below ()
- "Boundary: full-frame window -> default 'below for F12."
- (save-window-excursion
- (delete-other-windows)
- (should (eq (cj/--vterm-toggle-window-direction (selected-window))
- 'below))))
-
-(ert-deftest test-vterm-toggle--window-direction-below-split ()
- "Normal: bottom window of horizontal split -> 'below."
- (save-window-excursion
- (delete-other-windows)
- (let ((below (split-window (selected-window) nil 'below)))
- (should (eq (cj/--vterm-toggle-window-direction below) 'below)))))
-
-(ert-deftest test-vterm-toggle--window-direction-right-split ()
- "Normal: right window of vertical split -> 'right."
- (save-window-excursion
- (delete-other-windows)
- (let ((right (split-window (selected-window) nil 'right)))
- (should (eq (cj/--vterm-toggle-window-direction right) 'right)))))
-
-(ert-deftest test-vterm-toggle--window-size-returns-body ()
- "Normal: returns body-width for right/left, body-height for below/above."
- (save-window-excursion
- (delete-other-windows)
- (let ((below (split-window (selected-window) nil 'below)))
- (should (= (cj/--vterm-toggle-window-size below 'below)
- (window-body-height below))))))
-
(ert-deftest test-vterm-toggle--capture-state-records-direction-and-size ()
"Normal: capture-state writes direction and integer body size."
(save-window-excursion