296 lines
12 KiB
EmacsLisp
296 lines
12 KiB
EmacsLisp
|
;;; zoom.el --- Fixed and automatic balanced window layout
|
||
|
|
||
|
;; Copyright (c) 2022 Andrea Cardaci <cyrus.and@gmail.com>
|
||
|
;;
|
||
|
;; Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
;; of this software and associated documentation files (the "Software"), to deal
|
||
|
;; in the Software without restriction, including without limitation the rights
|
||
|
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
;; copies of the Software, and to permit persons to whom the Software is
|
||
|
;; furnished to do so, subject to the following conditions:
|
||
|
;;
|
||
|
;; The above copyright notice and this permission notice shall be included in
|
||
|
;; all copies or substantial portions of the Software.
|
||
|
;;
|
||
|
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
|
;; SOFTWARE.
|
||
|
|
||
|
;; Author: Andrea Cardaci <cyrus.and@gmail.com>
|
||
|
;; Version: 0.2.4
|
||
|
;; Package-Version: 20220411.1126
|
||
|
;; Package-Commit: 2104abb074682db79b9ff3a748e8e2e760a4d8cf
|
||
|
;; URL: https://github.com/cyrus-and/zoom
|
||
|
;; Package-Requires: ((emacs "24.4"))
|
||
|
;; Keywords: frames
|
||
|
|
||
|
;;; Commentary:
|
||
|
|
||
|
;; This minor mode takes care of managing the window sizes by enforcing a fixed
|
||
|
;; and automatic balanced layout where the currently selected window is resized
|
||
|
;; according to `zoom-size' which can be an absolute value in lines/columns, a
|
||
|
;; ratio between the selected window and frame size or even a custom callback.
|
||
|
|
||
|
;;; Code:
|
||
|
|
||
|
(defgroup zoom nil
|
||
|
"Enforce a fixed and automatic balanced window layout."
|
||
|
:group 'windows)
|
||
|
|
||
|
(defcustom zoom-size '(80 . 24)
|
||
|
"Size hint for the selected window.
|
||
|
|
||
|
It can be either a cons or a function.
|
||
|
|
||
|
Each component of the cons can be either an absolute value in
|
||
|
lines/columns or a ratio between the selected window and the
|
||
|
frame size. In the former case the window is resized according
|
||
|
to its body size, i.e., the total window size can be much larger.
|
||
|
In any case, windows are never shrinked if they are already
|
||
|
larger than the resulting size.
|
||
|
|
||
|
The function takes no arguments and returns a cons as specified
|
||
|
above."
|
||
|
:type '(choice (function :tag "Custom")
|
||
|
(cons :tag "Fixed"
|
||
|
(choice (integer :tag "Columns")
|
||
|
(float :tag "Width ratio"))
|
||
|
(choice (integer :tag "Lines")
|
||
|
(float :tag "Height ratio"))))
|
||
|
:safe #'consp
|
||
|
:group 'zoom)
|
||
|
|
||
|
(defcustom zoom-ignored-major-modes nil
|
||
|
"List of ignored major modes.
|
||
|
|
||
|
Selected windows using any of these major modes should not be
|
||
|
enlarged (only balanced)."
|
||
|
:type '(repeat symbol)
|
||
|
:group 'zoom)
|
||
|
|
||
|
(defcustom zoom-ignored-buffer-names nil
|
||
|
"List of ignored buffer names.
|
||
|
|
||
|
Selected windows displaying any of these buffers should not be
|
||
|
enlarged (only balanced)."
|
||
|
:type '(repeat string)
|
||
|
:group 'zoom)
|
||
|
|
||
|
(defcustom zoom-ignored-buffer-name-regexps nil
|
||
|
"List of ignored buffer name regexps.
|
||
|
|
||
|
Selected windows displaying buffers matching any of these regexps
|
||
|
should not be enlarged (only balanced)."
|
||
|
:type '(repeat regexp)
|
||
|
:group 'zoom)
|
||
|
|
||
|
(defcustom zoom-ignore-predicates nil
|
||
|
"List of additional predicates that allow to ignore windows.
|
||
|
|
||
|
These functions are called (in order) to decide whether the
|
||
|
selected window should be ignored (only balanced) or not.
|
||
|
Predicates take no parameters and as soon as one function returns
|
||
|
a non-nil value, the selected window is ignored and the others
|
||
|
are not called."
|
||
|
:type '(repeat function)
|
||
|
:group 'zoom)
|
||
|
|
||
|
(defcustom zoom-minibuffer-preserve-layout t
|
||
|
"Non-nil means that the layout is retained when the minubuffer is entered.
|
||
|
|
||
|
Otherwise, since the minibuffer cannot be zoomed, all the other
|
||
|
windows are simply balanced. Setting this variable to nil can be
|
||
|
useful when third-party modes use the minibuffer to display more
|
||
|
than few lines."
|
||
|
:type 'boolean
|
||
|
:group 'zoom)
|
||
|
|
||
|
;;;###autoload
|
||
|
(define-minor-mode zoom-mode
|
||
|
"Perform `zoom' automatically as the selected window changes."
|
||
|
:global t
|
||
|
:lighter " Z"
|
||
|
:require 'zoom
|
||
|
(if zoom-mode
|
||
|
(zoom--on)
|
||
|
(zoom--off)))
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun zoom ()
|
||
|
"Zoom the current window and balance the others according to `zoom-size'."
|
||
|
(interactive)
|
||
|
;; manual invocation only works when this mode is disabled
|
||
|
(if zoom-mode
|
||
|
(message "Window zooming is automatic (M-x zoom-mode to disable)")
|
||
|
(zoom--update)))
|
||
|
|
||
|
(defun zoom--on ()
|
||
|
"Enable hooks and advices and update the layout."
|
||
|
;; register the zoom handler
|
||
|
(add-function :after pre-redisplay-function #'zoom--handler)
|
||
|
;; disable mouse resizing
|
||
|
(advice-add #'mouse-drag-mode-line :override #'ignore)
|
||
|
(advice-add #'mouse-drag-vertical-line :override #'ignore)
|
||
|
(advice-add #'mouse-drag-header-line :override #'ignore)
|
||
|
;; update the layout once loaded
|
||
|
(dolist (frame (frame-list))
|
||
|
(with-selected-frame frame
|
||
|
(zoom--handler))))
|
||
|
|
||
|
(defun zoom--off ()
|
||
|
"Disable hooks and advices and evenly balance the windows."
|
||
|
;; unregister the zoom handler
|
||
|
(remove-function pre-redisplay-function #'zoom--handler)
|
||
|
;; enable mouse resizing
|
||
|
(advice-remove #'mouse-drag-mode-line #'ignore)
|
||
|
(advice-remove #'mouse-drag-vertical-line #'ignore)
|
||
|
(advice-remove #'mouse-drag-header-line #'ignore)
|
||
|
;; leave with a clean layout
|
||
|
(dolist (frame (frame-list))
|
||
|
(balance-windows frame)))
|
||
|
|
||
|
(defvar zoom--last-window nil
|
||
|
"Keep track of the currently selected window.")
|
||
|
|
||
|
(defun zoom--get-frame-snapshot ()
|
||
|
"Get a snapshot of the current frame.
|
||
|
|
||
|
The return value is used to determine if an update is needed."
|
||
|
;; the windows size is added to update the layout during the frame resize (XXX
|
||
|
;; note that the simple frame size is not enough because for some reason it's
|
||
|
;; not properly updated during maximize/restore operations); the mouse
|
||
|
;; tracking invalidates the condition so it is possible to delay the update
|
||
|
;; during a mouse tracking event; the list is converted to string to also
|
||
|
;; compare the buffer (the order of the list places the selected window first)
|
||
|
|
||
|
;; TODO adding the window sizes here causes one spurious update because first
|
||
|
;; the selected window is changed then the resize happens
|
||
|
(format "%s" (list (default-value 'track-mouse)
|
||
|
(mapcar (lambda (window) (list window
|
||
|
(window-total-width)
|
||
|
(window-total-height)))
|
||
|
(window-list)))))
|
||
|
|
||
|
(defun zoom--handler (&optional ignored)
|
||
|
"Handle an update event.
|
||
|
|
||
|
Argument IGNORED is ignored."
|
||
|
;; check if the windows have changed since the last time
|
||
|
(let ((snapshot (zoom--get-frame-snapshot)))
|
||
|
(unless (equal (frame-parameter nil 'zoom--frame-snapshot) snapshot)
|
||
|
;; update the windows snapshot
|
||
|
(set-frame-parameter nil 'zoom--frame-snapshot snapshot)
|
||
|
;; zoom the previously selected window if a mouse tracking is in progress
|
||
|
;; of if the minibuffer is selected (according to the user preference)
|
||
|
(with-selected-window
|
||
|
(if (or (equal (selected-window) zoom--last-window)
|
||
|
(and zoom-minibuffer-preserve-layout (window-minibuffer-p))
|
||
|
(default-value 'track-mouse))
|
||
|
zoom--last-window
|
||
|
;; XXX this can't be simply omitted because it's needed to address
|
||
|
;; the case where a window changes buffer from/to a ignored buffer
|
||
|
(selected-window))
|
||
|
;; update the currently zoomed window
|
||
|
(setq zoom--last-window (selected-window))
|
||
|
(zoom--update)))))
|
||
|
|
||
|
(defun zoom--update ()
|
||
|
"Update the window layout in the current frame."
|
||
|
(let (;; temporarily disables this mode during resize to avoid infinite
|
||
|
;; recursion (probably not needed thanks to the following)
|
||
|
(zoom-mode nil)
|
||
|
;; temporarily disable all (even external) hooks about window
|
||
|
;; configuration changes to try to avoid potential flickering since
|
||
|
;; `balance-windows' calls them
|
||
|
(window-configuration-change-hook nil)
|
||
|
;; make sure that other windows are resized nicely after resizing the
|
||
|
;; selected one
|
||
|
(window-combination-resize t)
|
||
|
;; make sure that the exact same amount of pixels is assigned to all the
|
||
|
;; siblings
|
||
|
(window-resize-pixelwise t))
|
||
|
;; start from a balanced layout anyway
|
||
|
(balance-windows)
|
||
|
;; check if the selected window is not ignored
|
||
|
(unless (zoom--window-ignored-p)
|
||
|
(zoom--resize)
|
||
|
;; fix the scrolling but not for image buffers
|
||
|
(unless (derived-mode-p 'image-mode)
|
||
|
(zoom--fix-scroll)))))
|
||
|
|
||
|
(defun zoom--window-ignored-p ()
|
||
|
"Check whether the selected window will be ignored or not."
|
||
|
(or
|
||
|
;; `one-window-p' does not work well with the completion buffer
|
||
|
;; when emacsclient is used
|
||
|
(frame-root-window-p (selected-window))
|
||
|
;; never attempt to zoom the minibuffer
|
||
|
(window-minibuffer-p)
|
||
|
;; check against the major mode
|
||
|
(member major-mode zoom-ignored-major-modes)
|
||
|
;; check against the buffer name
|
||
|
(member (buffer-name) zoom-ignored-buffer-names)
|
||
|
;; check against the buffer name (using a regexp)
|
||
|
(catch 'ignored
|
||
|
(dolist (regex zoom-ignored-buffer-name-regexps)
|
||
|
(when (string-match regex (buffer-name))
|
||
|
(throw 'ignored t))))
|
||
|
;; check user-defined predicates
|
||
|
(catch 'ignored
|
||
|
(dolist (predicate zoom-ignore-predicates)
|
||
|
(when (funcall predicate)
|
||
|
(throw 'ignored t))))))
|
||
|
|
||
|
(defun zoom--resize ()
|
||
|
"Resize the selected window according to the user preference."
|
||
|
(let ((size-hint-cons
|
||
|
;; either use the cons as is or call the custom function
|
||
|
(if (functionp zoom-size) (funcall zoom-size) zoom-size)))
|
||
|
(zoom--resize-one-dimension size-hint-cons t)
|
||
|
(zoom--resize-one-dimension size-hint-cons nil)))
|
||
|
|
||
|
(defun zoom--resize-one-dimension (size-hint-cons horizontal)
|
||
|
"Resize one dimension of the selected window according to the user preference.
|
||
|
|
||
|
Argument SIZE-HINT-CONS is the size hint provided by the user.
|
||
|
|
||
|
Argument HORIZONTAL determines whether the window should be
|
||
|
resized horizontally or vertically."
|
||
|
(let* ((size-hint
|
||
|
(if horizontal (car size-hint-cons) (cdr size-hint-cons)))
|
||
|
(frame-size
|
||
|
(if horizontal (frame-width) (frame-height)))
|
||
|
;; use the total size (including fringes, scroll bars, etc.) for ratios
|
||
|
;; and the body size for absolute values
|
||
|
(window-size
|
||
|
(if (floatp size-hint)
|
||
|
(if horizontal (window-total-width) (window-total-height))
|
||
|
(if horizontal (window-body-width) (window-body-height))))
|
||
|
;; either use an absolute value or a ratio
|
||
|
(min-window-size
|
||
|
(if (floatp size-hint) (round (* size-hint frame-size)) size-hint))
|
||
|
;; do not shrink the window if it is already large enough
|
||
|
(desired-delta (max (- min-window-size window-size) 0))
|
||
|
;; fall back to the maximum available if the windows are too small
|
||
|
(delta (window-resizable nil desired-delta horizontal)))
|
||
|
;; actually resize the window
|
||
|
(window-resize nil delta horizontal)))
|
||
|
|
||
|
(defun zoom--fix-scroll ()
|
||
|
"Fix the horizontal scrolling if needed."
|
||
|
;; scroll all the way to the left border
|
||
|
(scroll-right (window-hscroll))
|
||
|
;; if the window is not wide enough to contain the point scroll to center
|
||
|
;; unless lines are not truncated
|
||
|
(when (and truncate-lines
|
||
|
(> (current-column) (- (window-body-width) hscroll-margin)))
|
||
|
(scroll-left (- (current-column) (/ (window-body-width) 2)))))
|
||
|
|
||
|
(provide 'zoom)
|
||
|
|
||
|
;;; zoom.el ends here
|