summaryrefslogtreecommitdiff
path: root/home/dots/.emacs.d/elisp/exwm-mff.el
blob: 6b1ef8737d551a87025defc136cfbc177889549a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
;;; exwm-mff.el --- Mouse Follows Focus           -*- lexical-binding: t; -*-

;; Copyright (C) 2019, 2020, 2021  Ian Eure

;; Author: Ian Eure <public@lowbar.fyi>
;; URL: https://github.com/ieure/exwm-mff
;; Version: 1.2.1
;; Package-Requires: ((emacs "25.1"))
;; Keywords: unix

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Mouse Follows Focus
;; ===================
;;
;; Traditional window managers are mouse-centric: the window to receive
;; input is usually selected with the pointing device.
;;
;; Emacs is keybord-centric: the window to receive key input is usually
;; selected with the keyboard.  When you use the keyboard to focus a
;; window, the spatial relationship between pointer and active window is
;; broken -- the pointer can be anywhere on the screen, instead of over
;; the active window, which can make it hard to find.
;;
;; The same problem also exists in traditional windowing systems when
;; you use the keyboard to switch windows, e.g. with Alt-Tab.
;;
;; Because Emacs’ model is inverted, this suggests that the correct
;; behavior is also the inverse -- instead of using the mouse to
;; select a window to receive keyboard input, the keyboard should be
;; used to select the window to receive mouse input.
;;
;; `EXWM-MFF-MODE' is a global minor mode which does exactly this.
;; When the selected window in Emacs changes, the mouse pointer is
;; moved to its center, unless the pointer is already somewhere inside
;; the window’s bounds.  While it's especially helpful for for EXWM
;; users, it works for any Emacs window in a graphical session.
;;
;; This package also offers the `EXWM-MFF-WARP-TO-SELECTED' command,
;; which allows you to summon the pointer with a hotkey.  Unlike the
;; minor mode, summoning is unconditional, and will place the pointer in
;; the center of the window even if it already resides within its bounds
;; -- a handy feature if you’ve lost your pointer, even if you’re using
;; the minor mode.
;;
;;
;; Limitations
;; ~~~~~~~~~~~
;;
;; None known at this time.

;;; Code:

(require 'subr-x)
(require 'cl-macs)

(defcustom exwm-mff-ignore-if nil
  "List of predicate functions for windows to ignore.

Predicates accept one argument, WINDOW, and return non-NIL if
automatic pointer warping should be suppressed."
  :type 'hook
  :group 'exwm-mff)

(defconst exwm-mff--debug-buffer " *exwm-mff-debug*"
  "Name of the buffer exwm-mff will write debug messages into.")

(defvar exwm-mff--debug 0
  "Whether (and how) to debug exwm-mff.
0 = don't debug.
1 = log messages to *exwm-mff-debug*.
2 = log messages to *exwm-mff-debug* and the echo area.")

(defvar exwm-mff--last-window nil
  "The last selected window.")

(defun exwm-mff--guard ()
  "Raise an error unless this is a graphic session with mouse support."
  (unless (and (display-graphic-p) (display-mouse-p))
    (error "EXWM-MFF-MODE doesn't work on non-graphic or non-mouse sessions")))

(defun exwm-mff--contains-pointer? (window)
  "Return non-NIL when the mouse pointer is within FRAME and WINDOW."
  (cl-destructuring-bind ((mouse-x . mouse-y) (left top right bottom))
      (list (mouse-absolute-pixel-position)
            (window-absolute-pixel-edges window))
    (and (<= left mouse-x right)
         (<= top mouse-y bottom))))

(defun exwm-mff--debug (string &rest objects)
  "Log debug message STRING, using OBJECTS to format it."
  (let ((debug-level (or exwm-mff--debug 0)))
    (when (> debug-level 0)
      (let ((str (apply #'format (concat "[%s] " string)
                        (cons (current-time-string) objects))))
        (when (>= debug-level 1)
          (with-current-buffer (get-buffer-create exwm-mff--debug-buffer)
            (goto-char (point-max))
            (insert (concat str "\n")))
          (when (>= debug-level 2)
            (message str)))))))

(defun exwm-mff-show-debug ()
  "Enable exwm-mff debugging, and show the buffer with debug logs."
  (interactive)
  (setq exwm-mff--debug 1)
  (pop-to-buffer (get-buffer-create exwm-mff--debug-buffer)))

(defun exwm-mff--window-center (window)
  "Return a list of (x y) coordinates of the center of WINDOW in FRAME."
  (cl-destructuring-bind (left top right bottom) (window-pixel-edges window)
    (list (+ left (/ (- right left) 2))
          (+ top (/ (- bottom top) 2)))))

(defun exwm-mff-warp-to (frame window)
  "Place the pointer in the center of WINDOW in FRAME."
  (apply #'set-mouse-pixel-position frame
         (exwm-mff--window-center window)))

;;;###autoload
(defun exwm-mff-warp-to-selected ()
  "Place the pointer in the center of the selected window."
  (interactive)
  (exwm-mff--guard)
  (exwm-mff-warp-to (selected-frame) (selected-window)))

(defun exwm-mff--explain (selected-window same-window? contains-pointer? mini? ignored?)
  "Use SELECTED-WINDOW, SAME-WINDOW?, CONTAINS-POINTER?, MINI?
and IGNORED? to return an explanation of focusing behavior."
  (cond
   (same-window? "selected window hasn't changed")
   (contains-pointer? "already contains pointer")
   (mini? "is minibuffer")
   (ignored? "one or more functions in `exwm-mff-ignore-if' matches")
   (t (format "doesn't contain pointer (in %s)" selected-window))))

(defun exwm-mff-hook (sw &optional norecord)
  "EXWM-MFF-MODE hook.

This is after-advice placed on SELECT-WINDOW.  It moves the
pointer to SW (the currently selected window), if NORECORD is
nil, and if it's not already in it."
  (unless norecord
    (if-let ((same-window? (eq sw exwm-mff--last-window)))
        ;; The selected window is unchanged, we don't need to check
        ;; anything else.
        (exwm-mff--debug
         "nop-> %s" (exwm-mff--explain sw same-window? nil nil nil))

      (let* ((sf (window-frame sw))
             (contains-pointer? (exwm-mff--contains-pointer? sw))
             (mini? (minibufferp (window-buffer sw)))
             (ignore? (run-hook-with-args-until-success 'exwm-mff-ignore-if sw)))
        (if (or same-window? contains-pointer? mini? ignore?)
            (exwm-mff--debug
             "nop-> %s::%s (%s)" sf sw (exwm-mff--explain sw nil contains-pointer? mini? ignore?))
          (exwm-mff--debug
           "warp-> %s::%s (%s)" sf sw (exwm-mff--explain sw nil contains-pointer? mini? ignore?))
          (exwm-mff-warp-to sf (setq exwm-mff--last-window sw)))))))

(defgroup exwm-mff nil
  "Mouse-Follows-Focus mode for EXWM."
  :group 'exwm)

;;;###autoload
(define-minor-mode exwm-mff-mode
  "Mouse follows focus mode for EXWM."
  :global t
  :require 'exwm-mff
  :group 'exwm-mff
  (exwm-mff--guard)
  (if exwm-mff-mode
      (advice-add 'select-window :after #'exwm-mff-hook)
    (advice-remove 'select-window #'exwm-mff-hook)))

(provide 'exwm-mff)

;;; exwm-mff.el ends here