* lisp/window-x.el (rotate-windows, rotate-windows-back): In doc-strings mention alternative functions.
469 lines
20 KiB
EmacsLisp
469 lines
20 KiB
EmacsLisp
;;; window-x.el --- Extra window related commands -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2025-2026 Free Software Foundation, Inc.
|
|
|
|
;; Author: Pranshu Sharma <pranshu@bauherren.ovh>
|
|
;; Martin Rudalics <rudalics@gmx.at>
|
|
;; Maintainer: emacs-devel@gnu.org
|
|
;; Keywords: window, convenience
|
|
;; Package: emacs
|
|
|
|
;; This file is part of GNU Emacs.
|
|
|
|
;; GNU Emacs 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.
|
|
|
|
;; GNU Emacs 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 GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; This file defines less frequently used window organization commands.
|
|
|
|
;;; Code:
|
|
|
|
(defun window-tree-normal-sizes (window &optional next)
|
|
"Return normal sizes of all windows rooted at WINDOW.
|
|
The return value is a list of the form (SPLIT-TYPE PARENT-WIN
|
|
PARENT-WIN-HEIGHT PARENT-WIN-WIDTH . WS), where SPLIT-TYPE is non-nil if
|
|
PARENT-WIN is split horizontally; PARENT-WIN is the internal window;
|
|
PARENT-WIN-HEIGHT and PARENT-WIN-WIDTH are the normal heights of
|
|
PARENT-WIN; and WS is a list of lists the form (WINDOW HEIGHT WIDTH)
|
|
where HEIGHT and WIDTH are the normal height and width of the window.
|
|
|
|
\(fn WINDOW)"
|
|
(let (list)
|
|
(while window
|
|
(setq list
|
|
(cons
|
|
(cond
|
|
((window-top-child window)
|
|
(append
|
|
(list t window
|
|
(window-normal-size window nil)
|
|
(window-normal-size window t))
|
|
(window-tree-normal-sizes (window-top-child window) t)))
|
|
((window-left-child window)
|
|
(append
|
|
(list nil window
|
|
(window-normal-size window nil)
|
|
(window-normal-size window t))
|
|
(window-tree-normal-sizes (window-left-child window) t)))
|
|
(t (list window
|
|
(window-normal-size window nil)
|
|
(window-normal-size window t))))
|
|
list))
|
|
(setq window (when next (window-next-sibling window))))
|
|
(nreverse list)))
|
|
|
|
(defsubst window--rotate-interactive-arg ()
|
|
"Return interactive window argument for window rotation commands."
|
|
(if current-prefix-arg (window-parent) (window-main-window)))
|
|
|
|
;;;###autoload
|
|
(defun window-layout-rotate-clockwise (&optional window)
|
|
"Rotate layout of WINDOW's child windows clockwise by 90 degrees.
|
|
WINDOW must be a parent window and defaults to the main window of the
|
|
selected frame. Interactively, with a prefix argument, rotate clockwise
|
|
the layout of the child windows of the selected window's parent. Signal
|
|
an error if WINDOW is not a parent window.
|
|
|
|
Recursively rotate the entire layout of WINDOW's child windows clockwise
|
|
by 90 degrees. Do not change the selected window of WINDOW's frame. If
|
|
you want to rotate windows within their frame's layout, consider using
|
|
`rotate-windows' instead."
|
|
(interactive (list (window--rotate-interactive-arg)))
|
|
(window--transpose (or window (window-main-window)) '(left . below) nil))
|
|
|
|
;;;###autoload
|
|
(defun window-layout-rotate-anticlockwise (&optional window)
|
|
"Rotate layout of WINDOW's child windows counterclockwise by 90 degrees.
|
|
WINDOW must be a parent window and defaults to the main window of the
|
|
selected frame. Interactively, with a prefix argument, rotate
|
|
counterclockwise the layout of the child windows of the selected
|
|
window's parent. Signal an error if WINDOW is not a parent window.
|
|
|
|
Recursively rotate the entire layout of WINDOW's child windows
|
|
counterclockwise by 90 degrees. Do not change the selected window of
|
|
WINDOW's frame. If you want to rotate windows within their frame's
|
|
layout, consider using `rotate-windows-back' instead."
|
|
(interactive (list (window--rotate-interactive-arg)))
|
|
(window--transpose (or window (window-main-window)) '(right . above) nil))
|
|
|
|
;;;###autoload
|
|
(defun window-layout-flip-leftright (&optional window)
|
|
"Flip WINDOW's child windows horizontally.
|
|
WINDOW must be a parent window and defaults to the main window of the
|
|
selected frame. Interactively, with a prefix argument, flip
|
|
horizontally the layout of the child windows of the selected window's
|
|
parent. Signal an error if WINDOW is not a parent window.
|
|
|
|
Recursively flip the layout of WINDOW's child windows so that a child
|
|
window on the right becomes a child window on the left and vice-versa."
|
|
(interactive (list (window--rotate-interactive-arg)))
|
|
(window--transpose (or window (window-main-window)) '(below . left) t))
|
|
|
|
;;;###autoload
|
|
(defun window-layout-flip-topdown (&optional window)
|
|
"Flip WINDOW's child windows vertically.
|
|
WINDOW must be a parent window and defaults to the main window of the
|
|
selected frame. Interactively, with a prefix argument, flip vertically
|
|
the layout of the child windows of the selected window's parent. Signal
|
|
an error if WINDOW is not a parent window.
|
|
|
|
Recursively flip the layout of WINDOW's child windows so that a child
|
|
window on the top becomes a child window on the bottom and vice-versa."
|
|
(interactive (list (window--rotate-interactive-arg)))
|
|
(window--transpose (or window (window-main-window)) '(above . right) t))
|
|
|
|
;;;###autoload
|
|
(defun window-layout-transpose (&optional window)
|
|
"Transpose child windows of WINDOW.
|
|
WINDOW must be a parent window and defaults to the main window of the
|
|
selected frame. Interactively, with a prefix argument, transpose the
|
|
layout of the child windows of the selected window's parent. Signal an
|
|
error if WINDOW is not a parent window.
|
|
|
|
Recursively reorganize WINDOW's child windows so that each horizontal
|
|
split becomes a vertical split and vice versa."
|
|
(interactive (list (window--rotate-interactive-arg)))
|
|
(window--transpose (or window (window-main-window)) '(right . below) nil))
|
|
|
|
(defcustom rotate-windows-change-selected t
|
|
"Whether rotating windows will change the selected window.
|
|
If this is nil, `rotate-windows' and `rotate-windows-back' do not change
|
|
a frame's selected window. Otherwise, the new selected window will be
|
|
the window that appears at the location of the previously selected
|
|
window."
|
|
:type 'boolean
|
|
:version "31.1"
|
|
:group 'windows)
|
|
|
|
;;;###autoload
|
|
(defun rotate-windows (&optional window reverse)
|
|
"Rotate child windows of WINDOW in cyclic ordering.
|
|
WINDOW must be a parent window and defaults to the main window of the
|
|
selected frame. Interactively, with a prefix argument, rotate the child
|
|
windows of the selected window's parent.
|
|
|
|
Optional argument REVERSE non-nil means to rotate windows backwards, in
|
|
reverse cyclic order. Signal an error if WINDOW is not a parent window,
|
|
all descendants of WINDOW are dedicated or some windows are of fixed
|
|
size or atomic.
|
|
|
|
Rotating windows leaves the way a frame layout has been produced via
|
|
splitting, deleting and resizing windows unaltered. It only \"moves\"
|
|
windows within that layout such that the space formerly occupied by any
|
|
window is now occupied by the window preceding (following if REVERSE is
|
|
non-nil) it in the cycling ordering.
|
|
|
|
If you want to rotate the entire layout of windows, consider using the
|
|
function `window-layout-rotate-clockwise' instead. If you want to
|
|
rotate a layout twice in a row in order to have a window on the bottom
|
|
appear on the top and a window on the right appear on the left (or
|
|
vice-versa), consider running `window-layout-flip-leftright' and
|
|
`window-layout-flip-topdown' instead."
|
|
(interactive (list (window--rotate-interactive-arg)))
|
|
(setq window (or window (window-main-window)))
|
|
(when (window-live-p window)
|
|
(user-error "No windows to rotate"))
|
|
(let* ((frame (window-frame window))
|
|
(selected-window (frame-selected-window window))
|
|
(x-y (and rotate-windows-change-selected
|
|
(cons (1+ (window-pixel-left selected-window))
|
|
(1+ (window-pixel-top selected-window)))))
|
|
(win-tree (car (window-tree-normal-sizes window)))
|
|
(winls (or
|
|
(seq-filter
|
|
(lambda (win)
|
|
(and (window-live-p win)
|
|
(not (window-dedicated-p win))))
|
|
(flatten-list win-tree))
|
|
;; Do we really care - the window/buffer relationship
|
|
;; should not be affected by rotating.
|
|
(user-error "All windows are dedicated")))
|
|
(rotated-ls (if reverse
|
|
(append (cdr winls) (list (car winls)))
|
|
(append (last winls) winls)))
|
|
(first-window (car rotated-ls))
|
|
(new-win-tree
|
|
;; Recursively process `win-tree' and construct a new tree
|
|
;; with the same shape and rotated windows at the leaves.
|
|
(named-let rec ((tree win-tree))
|
|
(cond
|
|
((consp tree) (cons (rec (car tree)) (rec (cdr tree))))
|
|
((and (window-live-p tree)
|
|
(not (window-dedicated-p tree)))
|
|
(pop rotated-ls))
|
|
(t tree)))))
|
|
(when (or (seq-some #'window-atom-root winls)
|
|
(seq-some #'window-fixed-size-p winls))
|
|
(user-error "Cannot rotate windows due to fixed size or atomic windows"))
|
|
(delete-other-windows-internal first-window window)
|
|
(window--transpose-1 new-win-tree first-window '(below . right) t nil)
|
|
(when x-y
|
|
;; Try to select the window at the position previously occupied by
|
|
;; the selected window.
|
|
(let ((window-at-x-y (window-at-x-y (car x-y) (cdr x-y) frame t)))
|
|
(when window-at-x-y
|
|
(setq selected-window window-at-x-y))))
|
|
|
|
(set-frame-selected-window frame selected-window)))
|
|
|
|
;;;###autoload
|
|
(defun rotate-windows-back (&optional window)
|
|
"Rotate child windows of WINDOW backwards in cyclic ordering.
|
|
WINDOW must be a parent window and defaults to the main window of the
|
|
selected frame. Interactively, with a prefix argument, rotate backwards
|
|
the child windows of the selected window's parent. Signal an error if
|
|
WINDOW is not a parent window, all descendants of WINDOW are dedicated
|
|
or some of them are of fixed size or atomic.
|
|
|
|
Rotating windows backwards leaves the way a frame layout has been
|
|
produced via splitting, deleting and resizing windows unaltered. It
|
|
only \"moves\" windows within that layout such that the space formerly
|
|
occupied by any window is now occupied by the window following it in
|
|
the cycling ordering.
|
|
|
|
If you want to rotate the entire layout of windows backwards, consider
|
|
using the function `window-layout-rotate-anticlockwise' instead. If you
|
|
want to rotate a layout backwards twice in a row, consider running
|
|
`window-layout-flip-leftright' and `window-layout-flip-topdown'
|
|
instead."
|
|
(interactive (list (window--rotate-interactive-arg)))
|
|
(rotate-windows window t))
|
|
|
|
(defun window--transpose (window conf no-resize)
|
|
"Rearrange windows under WINDOW recursively.
|
|
CONF should be a cons cell (HORIZONTAL-SPLIT . VERTICAL-SPLIT) where
|
|
HORIZONTAL-SPLIT will be used as the third argument of `split-window'
|
|
when splitting a window that was previously horizontally split, and
|
|
VERTICAL-SPLIT as third argument of `split-window' for a window that was
|
|
previously vertically split. If NO-RESIZE is nil, the SIDE argument of
|
|
the window-split is converted from vertical to horizontal or vice versa,
|
|
with the same proportion of the total split."
|
|
(when (or (not window) (window-live-p window))
|
|
(user-error "No windows to transpose"))
|
|
(let* ((frame (window-frame window))
|
|
(first-window window)
|
|
(selected-window (frame-selected-window window))
|
|
(win-tree (car (window-tree-normal-sizes window)))
|
|
(win-list (seq-filter #'window-live-p (flatten-list win-tree)))
|
|
(atom-windows (seq-keep #'window-atom-root win-list)))
|
|
(unless (and (not (eq (car atom-windows) window))
|
|
(or no-resize
|
|
(and (not atom-windows)
|
|
(not (seq-some #'window-fixed-size-p win-list)))))
|
|
(user-error "Cannot rotate windows due to fixed size or atom windows"))
|
|
(delete-dups atom-windows)
|
|
(while (not (window-live-p first-window))
|
|
(setq first-window (window-child first-window)))
|
|
(delete-other-windows-internal first-window window)
|
|
(window--transpose-1 win-tree first-window conf no-resize atom-windows)
|
|
;; Go back to previously selected window.
|
|
(set-frame-selected-window frame selected-window)
|
|
(mapc #'window-make-atom atom-windows)))
|
|
|
|
(defun window--transpose-1 (subtree cwin conf no-resize atom-windows)
|
|
"Subroutine of `window--transpose'.
|
|
SUBTREE must be in the format of the result of
|
|
`window-tree-normal-sizes'. CWIN is the current window through which
|
|
the window splits are made. ATOM-WINDOWS is a list of internal atom
|
|
windows. The CONF and NO-RESIZE arguments are the same as the
|
|
ones in `window--transpose'."
|
|
;; `flen' is max size the window could be converted to the opposite
|
|
;; of the given split type.
|
|
(let ((parent-window-is-set t)
|
|
;; Make sure combination resizing is turned off (Bug#81406).
|
|
window-combination-resize
|
|
(flen (if (xor no-resize (car subtree))
|
|
(float (window-pixel-width cwin))
|
|
(float (window-pixel-height cwin)))))
|
|
(mapc
|
|
(pcase-lambda (`(,window . ,size))
|
|
(prog1
|
|
(let* ((split-size (- (round (* flen size))))
|
|
(split-type
|
|
(funcall (if (car subtree) #'car #'cdr) conf))
|
|
(return-win
|
|
(if (listp window)
|
|
;; `window' is a window subtree.
|
|
;; `first-child' is a live window that is an descended of window
|
|
(let* ((first-child window)
|
|
;; If the window being split is atomic
|
|
(is-atom
|
|
;; cadr will return the internal parent window
|
|
(memq (cadr first-child) atom-windows)))
|
|
;; (caar (cddddr first-child)) is the first window in the
|
|
;; list if there is a live window.
|
|
(while (not (windowp (caar (cddddr first-child))))
|
|
(setq first-child (car (cddddr first-child))))
|
|
(window--transpose-1
|
|
window
|
|
(let ((window-combination-limit parent-window-is-set))
|
|
(split-window
|
|
cwin
|
|
split-size
|
|
split-type
|
|
t
|
|
(if window-combination-limit
|
|
(cons (caar (cddddr first-child)) (cadr subtree))
|
|
(caar (cddddr first-child)))))
|
|
(if is-atom '(nil . t) conf)
|
|
no-resize
|
|
atom-windows))
|
|
;; `window' is a window.
|
|
(split-window
|
|
cwin
|
|
split-size
|
|
split-type t
|
|
;; We need to set parent window if it hasn't been set
|
|
;; already.
|
|
(if parent-window-is-set
|
|
(cons window (cadr subtree))
|
|
window)))))
|
|
(when (eq window-combination-limit t)
|
|
(set-window-combination-limit (cadr subtree) nil))
|
|
return-win)
|
|
(setq parent-window-is-set nil)))
|
|
(mapcar
|
|
(lambda (e)
|
|
(pcase-let* ((`(,window . ,window-size-info)
|
|
(if (windowp (car e))
|
|
(cons (car e) e)
|
|
(cons e (cdr e)))))
|
|
(cons window
|
|
;; The respective size of the window.
|
|
(if (car subtree)
|
|
(cadr window-size-info)
|
|
(caddr window-size-info)))))
|
|
;; We need to ignore first 5 elements of window list, we ignore
|
|
;; window split type, sizes and the first window (it's
|
|
;; implicitly created). We just have a list of windows.
|
|
(nreverse (cdr (cddddr subtree)))))
|
|
;; (caar (cddddr subtree)) is the first child window of subtree.
|
|
(unless (windowp (caar (cddddr subtree)))
|
|
(let ((is-atom (memq (cadr (cadr (cddddr subtree))) atom-windows)))
|
|
(window--transpose-1 (car (cddddr subtree)) cwin
|
|
(if is-atom '(nil . t) conf)
|
|
no-resize atom-windows)))))
|
|
|
|
;;;###autoload
|
|
(defun merge-frames (&optional frame1 frame2 vertical)
|
|
"Merge the main window of FRAME2 into FRAME1.
|
|
Split the main window of FRAME1 and make the new window display the main
|
|
window of FRAME2. Both FRAME1 and FRAME2 must be live frames. FRAME1
|
|
defaults to the selected frame and FRAME2 to the frame that follows FRAME1
|
|
in the frame list.
|
|
If VERTICAL is non-nil, make the new window below the old main window of
|
|
FRAME1. Otherwise, make the new window on the right of FRAME1's main
|
|
window. Interactively, VERTICAL is the prefix argument, FRAME1 is the
|
|
selected frame and FRAME2 is the frame following FRAME1 in the frame
|
|
list. Delete FRAME2 if the merge completed successfully and return
|
|
FRAME1."
|
|
(interactive "i\ni\nP")
|
|
(let* ((frame1 (window-normalize-frame frame1))
|
|
(frame2 (or (if frame2
|
|
(window-normalize-frame frame2)
|
|
(next-frame frame1))
|
|
(user-error "Cannot find frame to merge"))))
|
|
(window-state-put
|
|
;; Source window on frame2.
|
|
(window-state-get (window-main-window frame2))
|
|
;; Make new window on frame1.
|
|
(split-window (window-main-window frame1) nil (not vertical)))
|
|
(delete-frame frame2)
|
|
frame1))
|
|
|
|
;;;###autoload
|
|
(defun window-get-split-combination (window arg)
|
|
"Return window combination suitable for `split-frame'.
|
|
|
|
WINDOW is the main window in which the combination should be derived.
|
|
ARG is the argument passed to `split-frame'. Return a
|
|
combination of windows `split-frame' is considered to split off."
|
|
(let* ((reverse (< arg 0))
|
|
;; This is where the pivot window is.
|
|
(total-window-count (window-child-count window))
|
|
(pivot-window-pos (- (if reverse
|
|
(+ total-window-count arg)
|
|
arg)
|
|
1))
|
|
(pivot-window (window-child window))
|
|
(active-window (frame-selected-window window))
|
|
;; If FRAME's selected window is on the left side of the
|
|
;; pivot window.
|
|
(active-window-on-left (eq pivot-window active-window)))
|
|
;; We want the 2nd level window that the active window is a
|
|
;; part of.
|
|
(while (not (eq (window-parent active-window) window))
|
|
(setq active-window (window-parent active-window)))
|
|
|
|
;; Now we need to find the pivot window
|
|
(dotimes (_ pivot-window-pos)
|
|
(setq pivot-window (window-next-sibling pivot-window))
|
|
(when (eq active-window pivot-window)
|
|
(setq active-window-on-left t)))
|
|
|
|
;; Now we have pivot-window set, and we just need to
|
|
;; combine. We want to split away all windows from the
|
|
;; side of the pivot that doesn't contain the active
|
|
;; window.
|
|
(let* ((first (window-child window))
|
|
(last (window-last-child window))
|
|
(next-pivot-sib (window-next-sibling pivot-window))
|
|
(right-comb (if (eq next-pivot-sib last)
|
|
last
|
|
(combine-windows next-pivot-sib last)))
|
|
(left-comb (if (eq first pivot-window)
|
|
first
|
|
(combine-windows first pivot-window))))
|
|
(if active-window-on-left right-comb left-comb))))
|
|
|
|
;;;###autoload
|
|
(defun split-frame (&optional frame arg)
|
|
"Split windows of specified FRAME into two separate frames.
|
|
FRAME must be a live frame and defaults to the selected frame. ARG
|
|
specifies the number of windows to consider for splitting and defaults
|
|
to 1. Interactively, ARG is the prefix argument.
|
|
|
|
First divide the child windows of FRAME's main window into two parts.
|
|
The first part includes the first ARG child windows if ARG is positive,
|
|
or -ARG last windows if it's negative. The second part includes the
|
|
remaining child windows of FRAME's main window. Then clone into a
|
|
newly-created frame each of the windows of the part which does not
|
|
include FRAME's selected window and delete those windows from FRAME.
|
|
|
|
Signal an error if ARG is either zero or not a number, if FRAME's main
|
|
window is live or does not have more child windows than specified by the
|
|
absolute value of ARG. Return the new frame."
|
|
(interactive "i\nP")
|
|
(let* ((frame (window-normalize-frame frame))
|
|
;; MAIN is FRAME'S main window.
|
|
(main (window-main-window frame))
|
|
(total-window-count (window-child-count main))
|
|
(arg (or arg 1)))
|
|
(cond
|
|
((window-live-p main)
|
|
(user-error "Cannot split frame with only one window"))
|
|
((or (not (numberp arg)) (zerop arg))
|
|
(user-error "Invalid ARG %s for splitting frame" arg))
|
|
((>= (abs arg) total-window-count)
|
|
(user-error "ARG %s exceeds number of windows %s that can be split off"
|
|
(abs arg) (1- total-window-count)))
|
|
(t
|
|
(let ((comb (window-get-split-combination main arg)))
|
|
(window-state-put (window-state-get comb)
|
|
(window-main-window (make-frame)))
|
|
(delete-window comb))))))
|
|
|
|
(provide 'window-x)
|
|
;;; window-x.el ends here
|