@@ -1582,6 +1582,206 @@ the reply waits for the redraw timer."
15821582 (ghostel--write-input term "\e[?1004l")
15831583 (should (equal nil (ghostel--focus-event term t))))) ; focus ignored after reset
15841584
1585+ ;; -----------------------------------------------------------------------
1586+ ;; Test: window-level focus events (issue #140)
1587+ ;; -----------------------------------------------------------------------
1588+
1589+ (defun ghostel-test--make-focus-buffer (name)
1590+ "Create a ghostel-mode buffer NAME with a fake term and live process.
1591+ Returns the buffer."
1592+ (let ((buf (generate-new-buffer name)))
1593+ (with-current-buffer buf
1594+ (ghostel-mode)
1595+ (setq ghostel--term (vector 'fake-term))
1596+ (setq ghostel--process
1597+ (start-process (concat "ghostel-test-focus-" name)
1598+ nil "cat"))
1599+ (set-process-query-on-exit-flag ghostel--process nil))
1600+ buf))
1601+
1602+ (defun ghostel-test--cleanup-focus-buffer (buf)
1603+ "Kill BUF and its fake process."
1604+ (when (buffer-live-p buf)
1605+ (with-current-buffer buf
1606+ (when (and ghostel--process (process-live-p ghostel--process))
1607+ (delete-process ghostel--process)))
1608+ (kill-buffer buf)))
1609+
1610+ (defmacro ghostel-test--with-focus-stub (events-var focus-fn &rest body)
1611+ "Run BODY with `ghostel--focus-event' and `frame-focus-state' stubbed.
1612+ EVENTS-VAR names a list that receives (BUFFER . FOCUSED) pairs.
1613+ FOCUS-FN is a zero-arg function returning the current `frame-focus-state'."
1614+ (declare (indent 2))
1615+ `(cl-letf (((symbol-function 'ghostel--focus-event)
1616+ (lambda (_term focused)
1617+ (push (cons (current-buffer) focused) ,events-var)
1618+ t))
1619+ ((symbol-function 'frame-focus-state)
1620+ (lambda (&optional _frame) (funcall ,focus-fn))))
1621+ ,@body))
1622+
1623+ (ert-deftest ghostel-test-focus-window-selection ()
1624+ "Window selection changes flip per-buffer focus state."
1625+ (let* ((events nil)
1626+ (focus-fn (lambda () t))
1627+ (buf (ghostel-test--make-focus-buffer " *ghostel-focus-1*"))
1628+ (other (generate-new-buffer " *other*"))
1629+ (saved-window-config (current-window-configuration)))
1630+ (unwind-protect
1631+ (ghostel-test--with-focus-stub events focus-fn
1632+ (delete-other-windows)
1633+ (switch-to-buffer buf)
1634+ (let ((other-win (split-window)))
1635+ (set-window-buffer other-win other)
1636+ ;; ghostel window selected → focus-in
1637+ (ghostel--focus-change)
1638+ (should (equal (car events) (cons buf t)))
1639+ ;; Select the other window → focus-out
1640+ (select-window other-win)
1641+ (setq events nil)
1642+ (ghostel--focus-change)
1643+ (should (equal (car events) (cons buf nil)))
1644+ ;; Select ghostel window again → focus-in
1645+ (select-window (get-buffer-window buf))
1646+ (setq events nil)
1647+ (ghostel--focus-change)
1648+ (should (equal (car events) (cons buf t)))))
1649+ (set-window-configuration saved-window-config)
1650+ (ghostel-test--cleanup-focus-buffer buf)
1651+ (kill-buffer other))))
1652+
1653+ (ert-deftest ghostel-test-focus-dedup ()
1654+ "Repeat calls with unchanged state do not re-send focus events."
1655+ (let* ((events nil)
1656+ (frame-focused t)
1657+ (focus-fn (lambda () frame-focused))
1658+ (buf (ghostel-test--make-focus-buffer " *ghostel-focus-dedup*"))
1659+ (saved-window-config (current-window-configuration)))
1660+ (unwind-protect
1661+ (ghostel-test--with-focus-stub events focus-fn
1662+ (delete-other-windows)
1663+ (switch-to-buffer buf)
1664+ (ghostel--focus-change) ; focus-in
1665+ (ghostel--focus-change) ; no-op (dedup)
1666+ (ghostel--focus-change) ; no-op (dedup)
1667+ (should (equal events (list (cons buf t))))
1668+ ;; Transition to focus-out, then confirm further calls dedup.
1669+ (setq frame-focused nil)
1670+ (ghostel--focus-change) ; focus-out
1671+ (ghostel--focus-change) ; no-op (dedup)
1672+ (should (equal events (list (cons buf nil) (cons buf t)))))
1673+ (set-window-configuration saved-window-config)
1674+ (ghostel-test--cleanup-focus-buffer buf))))
1675+
1676+ (ert-deftest ghostel-test-focus-two-ghostel-buffers ()
1677+ "Only the ghostel buffer in the selected window is focused."
1678+ (let* ((events nil)
1679+ (focus-fn (lambda () t))
1680+ (buf-a (ghostel-test--make-focus-buffer " *ghostel-focus-a*"))
1681+ (buf-b (ghostel-test--make-focus-buffer " *ghostel-focus-b*"))
1682+ (saved-window-config (current-window-configuration)))
1683+ (unwind-protect
1684+ (ghostel-test--with-focus-stub events focus-fn
1685+ (delete-other-windows)
1686+ (switch-to-buffer buf-a)
1687+ (let ((win-b (split-window)))
1688+ (set-window-buffer win-b buf-b)
1689+ ;; A selected: A transitions nil→t, B stays nil (dedup).
1690+ (ghostel--focus-change)
1691+ (should (equal events (list (cons buf-a t))))
1692+ ;; Select B: A transitions t→nil, B transitions nil→t.
1693+ (select-window win-b)
1694+ (setq events nil)
1695+ (ghostel--focus-change)
1696+ (should (= (length events) 2))
1697+ (should (member (cons buf-a nil) events))
1698+ (should (member (cons buf-b t) events))
1699+ ;; Back to A: inverse transitions.
1700+ (select-window (get-buffer-window buf-a))
1701+ (setq events nil)
1702+ (ghostel--focus-change)
1703+ (should (= (length events) 2))
1704+ (should (member (cons buf-a t) events))
1705+ (should (member (cons buf-b nil) events))))
1706+ (set-window-configuration saved-window-config)
1707+ (ghostel-test--cleanup-focus-buffer buf-a)
1708+ (ghostel-test--cleanup-focus-buffer buf-b))))
1709+
1710+ (ert-deftest ghostel-test-focus-frame-blur ()
1711+ "Frame losing focus drives the ghostel buffer to focus-out."
1712+ (let* ((events nil)
1713+ (frame-focused t)
1714+ (focus-fn (lambda () frame-focused))
1715+ (buf (ghostel-test--make-focus-buffer " *ghostel-focus-blur*"))
1716+ (saved-window-config (current-window-configuration)))
1717+ (unwind-protect
1718+ (ghostel-test--with-focus-stub events focus-fn
1719+ (delete-other-windows)
1720+ (switch-to-buffer buf)
1721+ (ghostel--focus-change) ; focus-in
1722+ (should (equal (car events) (cons buf t)))
1723+ (setq frame-focused nil) ; simulate app blur
1724+ (setq events nil)
1725+ (ghostel--focus-change)
1726+ (should (equal (car events) (cons buf nil)))
1727+ (setq frame-focused t) ; refocus
1728+ (setq events nil)
1729+ (ghostel--focus-change)
1730+ (should (equal (car events) (cons buf t))))
1731+ (set-window-configuration saved-window-config)
1732+ (ghostel-test--cleanup-focus-buffer buf))))
1733+
1734+ (ert-deftest ghostel-test-focus-skips-state-update-when-1004-off ()
1735+ "Dropped events (mode 1004 off) do not update cached focus state.
1736+ Otherwise, enabling 1004 after a focus change would dedup away the
1737+ first real focus event."
1738+ (let* ((events nil)
1739+ (emit-p nil)
1740+ (buf (ghostel-test--make-focus-buffer " *ghostel-focus-gated*"))
1741+ (saved-window-config (current-window-configuration)))
1742+ (unwind-protect
1743+ (cl-letf (((symbol-function 'ghostel--focus-event)
1744+ (lambda (_term focused)
1745+ (when emit-p
1746+ (push (cons (current-buffer) focused) events))
1747+ emit-p))
1748+ ((symbol-function 'frame-focus-state)
1749+ (lambda (&optional _frame) t)))
1750+ (delete-other-windows)
1751+ (switch-to-buffer buf)
1752+ ;; Mode 1004 off: event is dropped, state must remain nil.
1753+ (ghostel--focus-change)
1754+ (should (null events))
1755+ (with-current-buffer buf
1756+ (should (null ghostel--focus-state)))
1757+ ;; Child now enables mode 1004. Next focus-change must emit.
1758+ (setq emit-p t)
1759+ (ghostel--focus-change)
1760+ (should (equal events (list (cons buf t)))))
1761+ (set-window-configuration saved-window-config)
1762+ (ghostel-test--cleanup-focus-buffer buf))))
1763+
1764+ (ert-deftest ghostel-test-focus-minibuffer ()
1765+ "Activating the minibuffer triggers focus-out on the ghostel buffer."
1766+ (let* ((events nil)
1767+ (focus-fn (lambda () t))
1768+ (buf (ghostel-test--make-focus-buffer " *ghostel-focus-mini*"))
1769+ (saved-window-config (current-window-configuration)))
1770+ (unwind-protect
1771+ (ghostel-test--with-focus-stub events focus-fn
1772+ (delete-other-windows)
1773+ (switch-to-buffer buf)
1774+ (ghostel--focus-change)
1775+ (should (equal (car events) (cons buf t)))
1776+ ;; Simulate minibuffer activation by selecting the minibuffer window.
1777+ (let ((mb-win (minibuffer-window)))
1778+ (select-window mb-win)
1779+ (setq events nil)
1780+ (ghostel--focus-change)
1781+ (should (equal (car events) (cons buf nil)))))
1782+ (set-window-configuration saved-window-config)
1783+ (ghostel-test--cleanup-focus-buffer buf))))
1784+
15851785;; -----------------------------------------------------------------------
15861786;; Test: incremental (partial) redraw
15871787;; -----------------------------------------------------------------------
@@ -5942,7 +6142,13 @@ while :; do sleep 0.1; done'\n")
59426142
59436143
59446144(defconst ghostel-test--elisp-tests
5945- '(ghostel-test-raw-key-sequences
6145+ '(ghostel-test-focus-window-selection
6146+ ghostel-test-focus-dedup
6147+ ghostel-test-focus-two-ghostel-buffers
6148+ ghostel-test-focus-frame-blur
6149+ ghostel-test-focus-skips-state-update-when-1004-off
6150+ ghostel-test-focus-minibuffer
6151+ ghostel-test-raw-key-sequences
59466152 ghostel-test-modifier-number
59476153 ghostel-test-send-event
59486154 ghostel-test-raw-key-modified-specials
0 commit comments