frameset.el 53.9 KB
Newer Older
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
;;; frameset.el --- save and restore frame and window setup -*- lexical-binding: t -*-

;; Copyright (C) 2013 Free Software Foundation, Inc.

;; Author: Juanma Barranquero <lekktu@gmail.com>
;; Keywords: convenience

;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This file provides a set of operations to save a frameset (the state
;; of all or a subset of the existing frames and windows), both
;; in-session and persistently, and restore it at some point in the
;; future.
;;
;; It should be noted that restoring the frames' windows depends on
;; the buffers they are displaying, but this package does not provide
;; any way to save and restore sets of buffers (see desktop.el for
;; that).  So, it's up to the user of frameset.el to make sure that
;; any relevant buffer is loaded before trying to restore a frameset.
;; When a window is restored and a buffer is missing, the window will
;; be deleted unless it is the last one in the frame, in which case
;; some previous buffer will be shown instead.

;;; Code:

(require 'cl-lib)


44
(cl-defstruct (frameset (:type vector) :named
45 46
			;; Copier is defined below.
			(:copier nil))
47 48 49 50 51 52

  "A frameset encapsulates a serializable view of a set of frames and windows.

It contains the following slots, which can be accessed with
\(frameset-SLOT fs) and set with (setf (frameset-SLOT fs) VALUE):

53 54 55 56
  version      A read-only version number, identifying the format
		 of the frameset struct.  Currently its value is 1.
  timestamp    A read-only timestamp, the output of `current-time'.
  app          A symbol, or a list whose first element is a symbol, which
57 58 59
		 identifies the creator of the frameset and related info;
		 for example, desktop.el sets this slot to a list
		 `(desktop . ,desktop-file-version).
60 61
  name         A string, the name of the frameset instance.
  description  A string, a description for user consumption (to show in
62
		 menus, messages, etc).
63
  properties   A property list, to store both frameset-specific and
64 65 66
		 user-defined serializable data.
  states       A list of items (FRAME-PARAMETERS . WINDOW-STATE), in no
		 particular order.  Each item represents a frame to be
67
		 restored.  FRAME-PARAMETERS is a frame's parameter alist,
68
		 extracted with (frame-parameters FRAME) and filtered
69
		 through `frameset-filter-params'.
70
		 WINDOW-STATE is the output of `window-state-get' applied
71
		 to the root window of the frame.
72

73 74 75 76
To avoid collisions, it is recommended that applications wanting to add
private serializable data to `properties' either store all info under a
single, distinctive name, or use property names with a well-chosen prefix.

77 78
A frameset is intended to be used through the following simple API:

79 80
 - `frameset-save', the type's constructor, captures all or a subset of the
   live frames, and returns a serializable snapshot of them (a frameset).
81 82
 - `frameset-restore' takes a frameset, and restores the frames and windows
   it describes, as faithfully as possible.
83 84
 - `frameset-p' is the predicate for the frameset type.
 - `frameset-valid-p' checks a frameset's validity.
85 86 87 88 89
 - `frameset-copy' returns a deep copy of a frameset.
 - `frameset-prop' is a `setf'able accessor for the contents of the
   `properties' slot.
 - The `frameset-SLOT' accessors described above."

90 91 92 93 94 95 96
  (version     1              :read-only t)
  (timestamp   (current-time) :read-only t)
  (app         nil)
  (name        nil)
  (description nil)
  (properties  nil)
  (states      nil))
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
;; Add nicer docstrings for built-in predicate and accessors.
(put 'frameset-p 'function-documentation
     "Return non-nil if OBJECT is a frameset, nil otherwise.\n\n(fn OBJECT)")
(put 'frameset-version 'function-documentation
     "Return the version number of FRAMESET.\n
It is an integer that identifies the format of the frameset struct.
This slot cannot be modified.\n\n(fn FRAMESET)")
(put 'frameset-timestamp 'function-documentation
     "Return the creation timestamp of FRAMESET.\n
The value is in the format returned by `current-time'.
This slot cannot be modified.\n\n(fn FRAMESET)")
(put 'frameset-app 'function-documentation
     "Return the application identifier for FRAMESET.\n
The value is either a symbol, like `my-app', or a list
\(my-app ADDITIONAL-DATA...).\n\n(fn FRAMESET)")
(put 'frameset-name 'function-documentation
     "Return the name of FRAMESET (a string).\n\n(fn FRAMESET)")
(put 'frameset-description 'function-documentation
     "Return the description of FRAMESET (a string).\n\n(fn FRAMESET)")
(put 'frameset-properties 'function-documentation
     "Return the property list of FRAMESET.\n
This list is useful to store both frameset-specific and user-defined
serializable data.  The simplest way to access and modify it is
through `frameset-prop' (which see).\n\n(fn FRAMESET)")
(put 'frameset-states 'function-documentation
     "Return the list of frame states of FRAMESET.\n
A frame state is a pair (FRAME-PARAMETERS . WINDOW-STATE), where
FRAME-PARAMETERS is a frame's parameter alist, extracted with
\(frame-parameters FRAME) and filtered through `frameset-filter-params',
and WINDOW-STATE is the output of `window-state-get' applied to the
root window of the frame.\n
IMPORTANT: Modifying this slot may cause frameset functions to fail,
unless the type constraints defined above are respected.\n\n(fn FRAMESET)")

132 133 134
;;;###autoload (autoload 'frameset-p "frameset"
;;;###autoload   "Return non-nil if OBJECT is a frameset, nil otherwise." nil)

135
(defun frameset-copy (frameset)
136 137
  "Return a deep copy of FRAMESET.
FRAMESET is copied with `copy-tree'."
138 139
  (copy-tree frameset t))

140
;;;###autoload
141
(defun frameset-valid-p (object)
142 143 144 145
  "Return non-nil if OBJECT is a valid frameset, nil otherwise.

The return value is nil if OBJECT is not a frameset, or not
a valid one, and the frameset version if it is valid."
146
  (and (vectorp object)                   ; a vector
147
       (>= (length object) 8)             ; of the right length (future-proof)
148
       (eq (aref object 0) 'frameset)     ; tagged as `frameset'
149 150 151 152 153 154 155 156 157 158 159 160 161
       (integerp (aref object 1))         ; VERSION is an int
       (consp (aref object 2))            ; TIMESTAMP is a non-null list
       (let ((app (aref object 3)))
	 (or (null app)                   ; APP is nil
	     (symbolp app)                ; or a symbol
	     (and (consp app)             ; or a list
		  (symbolp (car app)))))  ; starting with a symbol
       (stringp (or (aref object 4) ""))  ; NAME is a string or nil
       (stringp (or (aref object 5) ""))  ; DESCRIPTION is a string or nil
       (listp (aref object 6))            ; PROPERTIES is a list
       (consp (aref object 7))            ; and STATES is non-nil
       (cl-every #'consp (aref object 7)) ; and an alist
       (aref object 1)))                  ; return VERSION
162

163
;; A setf'able accessor to the frameset's properties
164 165
(defun frameset-prop (frameset property)
  "Return the value for FRAMESET of PROPERTY.
166

167
Properties can be set with
168

169
  (setf (frameset-prop FRAMESET PROPERTY) NEW-VALUE)"
170
  (plist-get (frameset-properties frameset) property))
171

172 173 174 175 176 177
(gv-define-setter frameset-prop (val fs prop)
  (macroexp-let2 nil v val
    `(progn
       (setf (frameset-properties ,fs)
	     (plist-put (frameset-properties ,fs) ,prop ,v))
       ,v)))
178

179 180 181

;; Filtering

182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
;; What's the deal with these "filter alists"?
;;
;; Let's say that Emacs' frame parameters were never designed as a tool to
;; precisely record (or restore) a frame's state.  They grew organically,
;; and their uses and behaviors reflect their history.  In using them to
;; implement framesets, the unwary implementor, or the prospective package
;; writer willing to use framesets in their code, might fall victim of some
;; unexpected... oddities.
;;
;; You can find frame parameters that:
;;
;; - can be used to get and set some data from the frame's current state
;;   (`height', `width')
;; - can be set at creation time, and setting them afterwards has no effect
;;   (`window-state', `minibuffer')
;; - can be set at creation time, and setting them afterwards will fail with
;;   an error, *unless* you set it to the same value, a noop (`border-width')
;; - act differently when passed at frame creation time, and when set
;;   afterwards (`height')
;; - affect the value of other parameters (`name', `visibility')
;; - can be ignored by window managers (most positional args, like `height',
;;   `width', `left' and `top', and others, like `auto-raise', `auto-lower')
;; - can be set externally in X resources or Window registry (again, most
;;   positional parameters, and also `toolbar-lines', `menu-bar-lines' etc.)
;, - can contain references to live objects (`buffer-list', `minibuffer') or
;;   code (`buffer-predicate')
;; - are set automatically, and cannot be changed (`window-id', `parent-id'),
;;   but setting them produces no error
;; - have a noticeable effect in some window managers, and are ignored in
;;   others (`menu-bar-lines')
;; - can not be safely set in a tty session and then copied back to a GUI
;;   session (`font', `background-color', `foreground-color')
;;
;; etc etc.
;;
;; Which means that, in order to save a parameter alist to disk and read it
;; back later to reconstruct a frame, some processing must be done.  That's
;; what `frameset-filter-params' and the `frameset-*-filter-alist' variables
;; are for.
;;
222
;; First, a clarification.  The word "filter" in these names refers to both
223 224 225 226 227 228
;; common meanings of filter: to filter out (i.e., to remove), and to pass
;; through a transformation function (think `filter-buffer-substring').
;;
;; `frameset-filter-params' takes a parameter alist PARAMETERS, a filtering
;; alist FILTER-ALIST, and a flag SAVING to indicate whether we are filtering
;; parameters with the intent of saving a frame or restoring it.  It then
229
;; accumulates an output alist, FILTERED, by checking each parameter in
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
;; PARAMETERS against FILTER-ALIST and obeying any rule found there.  The
;; absence of a rule just means the parameter/value pair (called CURRENT in
;; filtering functions) is copied to FILTERED as is.  Keyword values :save,
;; :restore and :never tell the function to copy CURRENT to FILTERED in the
;; respective situations, that is, when saving, restoring, or never at all.
;; Values :save and :restore are not used in this package, because usually if
;; you don't want to save a parameter, you don't want to restore it either.
;; But they can be useful, for example, if you already have a saved frameset
;; created with some intent, and want to reuse it for a different objective
;; where the expected parameter list has different requirements.
;;
;; Finally, the value can also be a filtering function, or a filtering
;; function plus some arguments.  The function is called for each matching
;; parameter, and receives CURRENT (the parameter/value pair being processed),
;; FILTERED (the output alist so far), PARAMETERS (the full parameter alist),
;; SAVING (the save/restore flag), plus any additional ARGS set along the
;; function in the `frameset-*-filter-alist' entry.  The filtering function
;; then has the possibility to pass along CURRENT, or reject it altogether,
;; or pass back a (NEW-PARAM . NEW-VALUE) pair, which does not even need to
;; refer to the same parameter (so you can filter `width' and return `height'
;; and vice versa, if you're feeling silly and want to mess with the user's
;; mind).  As a help in deciding what to do, the filtering function has
;; access to PARAMETERS, but must not change it in any way.  It also has
;; access to FILTERED, which can be modified at will.  This allows two or
;; more filters to coordinate themselves, because in general there's no way
;; to predict the order in which they will be run.
;;
;; So, which parameters are filtered by default, and why? Let's see.
;;
;; - `buffer-list', `buried-buffer-list', `buffer-predicate': They contain
;;   references to live objects, or in the case of `buffer-predicate', it
;;   could also contain an fbound symbol (a predicate function) that could
;;   not be defined in a later session.
;;
;; - `window-id', `outer-window-id', `parent-id': They are assigned
;;   automatically and cannot be set, so keeping them is harmless, but they
;;   add clutter.  `window-system' is similar: it's assigned at frame
;;   creation, and does not serve any useful purpose later.
;;
;; - `left', `top': Only problematic when saving an iconified frame, because
;;   when the frame is iconified they are set to (- 32000), which doesn't
;;   really help in restoring the frame.  Better to remove them and let the
;;   window manager choose a default position for the frame.
;;
;; - `background-color', `foreground-color': In tty frames they can be set
;;   to "unspecified-bg" and "unspecified-fg", which aren't understood on
;;   GUI sessions.  They have to be filtered out when switching from tty to
;;   a graphical display.
;;
;; - `tty', `tty-type': These are tty-specific.  When switching to a GUI
280
;;   display they do no harm, but they clutter the parameter alist.
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
;;
;; - `minibuffer': It can contain a reference to a live window, which cannot
;;   be serialized.  Because of Emacs' idiosyncratic treatment of this
;;   parameter, frames created with (minibuffer . t) have a parameter
;;   (minibuffer . #<window...>), while frames created with
;;   (minibuffer . #<window...>) have (minibuffer . nil), which is madness
;;   but helps to differentiate between minibufferless and "normal" frames.
;;   So, changing (minibuffer . #<window...>) to (minibuffer . t) allows
;;   Emacs to set up the new frame correctly.  Nice, uh?
;;
;; - `name': If this parameter is directly set, `explicit-name' is
;;   automatically set to t, and then `name' no longer changes dynamically.
;;   So, in general, not saving `name' is the right thing to do, though
;;   surely there are applications that will want to override this filter.
;;
;; - `font', `fullscreen', `height' and `width': These parameters suffer
297
;;   from the fact that they are badly mangled when going through a
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
;;   tty session, though not all in the same way.  When saving a GUI frame
;;   and restoring it in a tty, the height and width of the new frame are
;;   those of the tty screen (let's say 80x25, for example); going back
;;   to a GUI session means getting frames of the tty screen size (so all
;;   your frames are 80 cols x 25 rows).  For `fullscreen' there's a
;;   similar problem, because a tty frame cannot really be fullscreen or
;;   maximized, so the state is lost.  The problem with `font' is a bit
;;   different, because a valid GUI font spec in `font' turns into
;;   (font . "tty") in a tty frame, and when read back into a GUI session
;;   it fails because `font's value is no longer a valid font spec.
;;
;; In most cases, the filtering functions just do the obvious thing: remove
;; CURRENT when it is meaningless to keep it, or pass a modified copy if
;; that helps (as in the case of `minibuffer').
;;
;; The exception are the parameters in the last set, which should survive
;; the roundtrip though tty-land.  The answer is to add "stashing
;; parameters", working in pairs, to shelve the GUI-specific contents and
;; restore it once we're back in pixel country.  That's what functions
317
;; `frameset-filter-shelve-param' and `frameset-filter-unshelve-param' do.
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
;;
;; Basically, if you set `frameset-filter-shelve-param' as the filter for
;; a parameter P, it will detect when it is restoring a GUI frame into a
;; tty session, and save P's value in the custom parameter X:P, but only
;; if X:P does not exist already (so it is not overwritten if you enter
;; the tty session more than once).  If you're not switching to a tty
;; frame, the filter just passes CURRENT along.
;;
;; The parameter X:P, on the other hand, must have been setup to be
;; filtered by `frameset-filter-unshelve-param', which unshelves the
;; value: if we're entering a GUI session, returns P instead of CURRENT,
;; while in other cases it just passes it along.
;;
;; The only additional trick is that `frameset-filter-shelve-param' does
;; not set P if switching back to GUI and P already has a value, because
;; it assumes that `frameset-filter-unshelve-param' did set it up.  And
;; `frameset-filter-unshelve-param', when unshelving P, must look into
;; FILTERED to determine if P has already been set and if so, modify it;
;; else just returns P.
;;
;; Currently, the value of X in X:P is `GUI', but you can use any prefix,
;; by passing its symbol as argument in the filter:
;;
;;   (my-parameter frameset-filter-shelve-param MYPREFIX)
;;
;; instead of
;;
;;   (my-parameter . frameset-filter-shelve-param)
;;
;; Note that `frameset-filter-unshelve-param' does not need MYPREFIX
;; because it is available from the parameter name in CURRENT.  Also note
;; that the colon between the prefix and the parameter name is hardcoded.
;; The reason is that X:P is quite readable, and that the colon is a
;; very unusual character in symbol names, other than in initial position
;; in keywords (emacs -Q has only two such symbols, and one of them is a
;; URL).  So the probability of a collision with existing or future
;; symbols is quite insignificant.
;;
356 357
;; Now, what about the filter alist variables? There are three of them,
;; though only two sets of parameters:
358 359 360 361 362 363 364 365 366 367 368 369 370
;;
;; - `frameset-session-filter-alist' contains these filters that allow to
;;   save and restore framesets in-session, without the need to serialize
;;   the frameset or save it to disk (for example, to save a frameset in a
;;   register and restore it later).  Filters in this list do not remove
;;   live objects, except in `minibuffer', which is dealt especially by
;;   `frameset-save' / `frameset-restore'.
;;
;; - `frameset-persistent-filter-alist' is the whole deal.  It does all
;;   the filtering described above, and the result is ready to be saved on
;;   disk without loss of information.  That's the format used by the
;;   desktop.el package, for example.
;;
371
;; IMPORTANT: These variables share structure and should NEVER be modified.
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
;;
;; - `frameset-filter-alist': The value of this variable is the default
;;   value for the FILTERS arguments of `frameset-save' and
;;   `frameset-restore'.  It is set to `frameset-persistent-filter-alist',
;;   though it can be changed by specific applications.
;;
;; How to use them?
;;
;; The simplest way is just do nothing.  The default should work
;; reasonably and sensibly enough.  But, what if you really need a
;; customized filter alist?  Then you can create your own variable
;;
;;   (defvar my-filter-alist
;;     '((my-param1 . :never)
;;       (my-param2 . :save)
;;       (my-param3 . :restore)
;;       (my-param4 . my-filtering-function-without-args)
;;       (my-param5   my-filtering-function-with arg1 arg2)
;;       ;;; many other parameters
;;       )
;;     "My customized parameter filter alist.")
;;
;; or, if you're only changing a few items,
;;
;;   (defvar my-filter-alist
;;     (nconc '((my-param1 . :never)
;;              (my-param2 . my-filtering-function))
;;            frameset-filter-alist)
;;     "My brief customized parameter filter alist.")
;;
;; and pass it to the FILTER arg of the save/restore functions,
;; ALWAYS taking care of not modifying the original lists; if you're
;; going to do any modifying of my-filter-alist, please use
;;
;;   (nconc '((my-param1 . :never) ...)
;;          (copy-sequence frameset-filter-alist))
;;
;; One thing you shouldn't forget is that they are alists, so searching
;; in them is sequential.  If you just want to change the default of
;; `name' to allow it to be saved, you can set (name . nil) in your
;; customized filter alist; it will take precedence over the latter
;; setting.  In case you decide that you *always* want to save `name',
;; you can add it to `frameset-filter-alist':
;;
;;   (push '(name . nil) frameset-filter-alist)
;;
;; In certain applications, having a parameter filtering function like
;; `frameset-filter-params' can be useful, even if you're not using
;; framesets.  The interface of `frameset-filter-params' is generic
;; and does not depend of global state, with one exception: it uses
;; the internal variable `frameset--target-display' to decide if, and
;; how, to modify the `display' parameter of FILTERED.  But that
;; should not represent any problem, because it's only meaningful
;; when restoring, and customized uses of `frameset-filter-params'
;; are likely to use their own filter alist and just call
;;
;;   (setq my-filtered (frameset-filter-params my-params my-filters t))
;;
;; In case you want to use it with the standard filters, you can
;; wrap the call to `frameset-filter-params' in a let form to bind
;; `frameset--target-display' to nil or the desired value.
;;

435
;;;###autoload
436
(defvar frameset-session-filter-alist
437
  '((name            . :never)
438
    (left            . frameset-filter-iconified)
439 440
    (minibuffer      . frameset-filter-minibuffer)
    (top             . frameset-filter-iconified))
441
  "Minimum set of parameters to filter for live (on-session) framesets.
442
DO NOT MODIFY.  See `frameset-filter-alist' for a full description.")
443 444 445 446

;;;###autoload
(defvar frameset-persistent-filter-alist
  (nconc
447 448 449
   '((background-color   . frameset-filter-sanitize-color)
     (buffer-list        . :never)
     (buffer-predicate   . :never)
450
     (buried-buffer-list . :never)
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
     (font               . frameset-filter-shelve-param)
     (foreground-color   . frameset-filter-sanitize-color)
     (fullscreen         . frameset-filter-shelve-param)
     (GUI:font           . frameset-filter-unshelve-param)
     (GUI:fullscreen     . frameset-filter-unshelve-param)
     (GUI:height         . frameset-filter-unshelve-param)
     (GUI:width          . frameset-filter-unshelve-param)
     (height             . frameset-filter-shelve-param)
     (outer-window-id    . :never)
     (parent-id          . :never)
     (tty                . frameset-filter-tty-to-GUI)
     (tty-type           . frameset-filter-tty-to-GUI)
     (width              . frameset-filter-shelve-param)
     (window-id          . :never)
     (window-system      . :never))
466 467
   frameset-session-filter-alist)
  "Parameters to filter for persistent framesets.
468
DO NOT MODIFY.  See `frameset-filter-alist' for a full description.")
469 470 471

;;;###autoload
(defvar frameset-filter-alist frameset-persistent-filter-alist
472 473
  "Alist of frame parameters and filtering functions.

474 475 476
This alist is the default value of the FILTERS argument of
`frameset-save' and `frameset-restore' (which see).

477 478 479 480 481 482 483 484 485
Initially, `frameset-filter-alist' is set to, and shares the value of,
`frameset-persistent-filter-alist'.  You can override any item in
this alist by `push'ing a new item onto it.  If, for some reason, you
intend to modify existing values, do

  (setq frameset-filter-alist (copy-tree frameset-filter-alist))

before changing anything.

486 487 488
On saving, PARAMETERS is the parameter alist of each frame processed,
and FILTERED is the parameter alist that gets saved to the frameset.

489 490
On restoring, PARAMETERS is the parameter alist extracted from the
frameset, and FILTERED is the resulting frame parameter alist used
491 492
to restore the frame.

493 494 495
Elements of `frameset-filter-alist' are conses (PARAM . ACTION),
where PARAM is a parameter name (a symbol identifying a frame
parameter), and ACTION can be:
496 497 498

 nil       The parameter is copied to FILTERED.
 :never    The parameter is never copied to FILTERED.
499
 :save     The parameter is copied only when saving the frame.
500
 :restore  The parameter is copied only when restoring the frame.
501
 FILTER    A filter function.
502 503

FILTER can be a symbol FILTER-FUN, or a list (FILTER-FUN ARGS...).
504 505 506 507 508
FILTER-FUN is invoked with

  (apply FILTER-FUN CURRENT FILTERED PARAMETERS SAVING ARGS)

where
509 510

 CURRENT     A cons (PARAM . VALUE), where PARAM is the one being
511
	     filtered and VALUE is its current value.
512
 FILTERED    The resulting alist (so far).
513
 PARAMETERS  The complete alist of parameters being filtered,
514
 SAVING      Non-nil if filtering before saving state, nil if filtering
515
	       before restoring it.
516
 ARGS        Any additional arguments specified in the ACTION.
517

518 519
FILTER-FUN is allowed to modify items in FILTERED, but no other arguments.
It must return:
520 521
 nil                      Skip CURRENT (do not add it to FILTERED).
 t                        Add CURRENT to FILTERED as is.
522 523 524 525
 (NEW-PARAM . NEW-VALUE)  Add this to FILTERED instead of CURRENT.

Frame parameters not on this alist are passed intact, as if they were
defined with ACTION = nil.")
526 527 528 529 530


(defvar frameset--target-display nil
  ;; Either (minibuffer . VALUE) or nil.
  ;; This refers to the current frame config being processed inside
531
  ;; `frameset-restore' and its auxiliary functions (like filtering).
532 533 534 535 536 537
  ;; If nil, there is no need to change the display.
  ;; If non-nil, display parameter to use when creating the frame.
  "Internal use only.")

(defun frameset-switch-to-gui-p (parameters)
  "True when switching to a graphic display.
538 539 540 541
Return non-nil if the parameter alist PARAMETERS describes a frame on a
text-only terminal, and the frame is being restored on a graphic display;
otherwise return nil.  Only meaningful when called from a filtering
function in `frameset-filter-alist'."
542 543 544
  (and frameset--target-display                   ; we're switching
       (null (cdr (assq 'display parameters)))    ; from a tty
       (cdr frameset--target-display)))           ; to a GUI display
545 546 547

(defun frameset-switch-to-tty-p (parameters)
  "True when switching to a text-only terminal.
548 549 550 551
Return non-nil if the parameter alist PARAMETERS describes a frame on a
graphic display, and the frame is being restored on a text-only terminal;
otherwise return nil.  Only meaningful when called from a filtering
function in `frameset-filter-alist'."
552 553 554
  (and frameset--target-display                   ; we're switching
       (cdr (assq 'display parameters))           ; from a GUI display
       (null (cdr frameset--target-display))))    ; to a tty
555

556
(defun frameset-filter-tty-to-GUI (_current _filtered parameters saving)
557 558 559
  "Remove CURRENT when switching from tty to a graphic display.

For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
560
see `frameset-filter-alist'."
561 562 563
  (or saving
      (not (frameset-switch-to-gui-p parameters))))

564 565
(defun frameset-filter-sanitize-color (current _filtered parameters saving)
  "When switching to a GUI frame, remove \"unspecified\" colors.
566 567 568
Useful as a filter function for tty-specific parameters.

For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
569
see `frameset-filter-alist'."
570 571 572 573 574 575
  (or saving
      (not (frameset-switch-to-gui-p parameters))
      (not (stringp (cdr current)))
      (not (string-match-p "^unspecified-[fb]g$" (cdr current)))))

(defun frameset-filter-minibuffer (current _filtered _parameters saving)
576
  "When saving, convert (minibuffer . #<window>) to (minibuffer . t).
577 578

For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
579
see `frameset-filter-alist'."
580 581 582 583 584
  (or (not saving)
      (if (windowp (cdr current))
	  '(minibuffer . t)
	t)))

585 586
(defun frameset-filter-shelve-param (current _filtered parameters saving
					     &optional prefix)
587
  "When switching to a tty frame, save parameter P as PREFIX:P.
588
The parameter can be later restored with `frameset-filter-unshelve-param'.
589 590 591
PREFIX defaults to `GUI'.

For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
592
see `frameset-filter-alist'."
593 594 595 596 597 598 599 600 601 602 603
  (unless prefix (setq prefix 'GUI))
  (cond (saving t)
	((frameset-switch-to-tty-p parameters)
	 (let ((prefix:p (intern (format "%s:%s" prefix (car current)))))
	   (if (assq prefix:p parameters)
	       nil
	     (cons prefix:p (cdr current)))))
	((frameset-switch-to-gui-p parameters)
	 (not (assq (intern (format "%s:%s" prefix (car current))) parameters)))
	(t t)))

604
(defun frameset-filter-unshelve-param (current filtered parameters saving)
605
  "When switching to a GUI frame, restore PREFIX:P parameter as P.
606 607 608
CURRENT must be of the form (PREFIX:P . value).

For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
609
see `frameset-filter-alist'."
610 611 612 613 614 615 616 617 618 619 620 621 622 623
  (or saving
      (not (frameset-switch-to-gui-p parameters))
      (let* ((prefix:p (symbol-name (car current)))
	     (p (intern (substring prefix:p
				   (1+ (string-match-p ":" prefix:p)))))
	     (val (cdr current))
	     (found (assq p filtered)))
	(if (not found)
	    (cons p val)
	  (setcdr found val)
	  nil))))

(defun frameset-filter-iconified (_current _filtered parameters saving)
  "Remove CURRENT when saving an iconified frame.
624
This is used for positional parameters `left' and `top', which are
625
meaningless in an iconified frame, so the frame is restored in a
626 627 628
default position.

For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
629
see `frameset-filter-alist'."
630 631 632
  (not (and saving (eq (cdr (assq 'visibility parameters)) 'icon))))

(defun frameset-filter-params (parameters filter-alist saving)
633
  "Filter parameter alist PARAMETERS and return a filtered alist.
634 635 636 637 638 639
FILTER-ALIST is an alist of parameter filters, in the format of
`frameset-filter-alist' (which see).
SAVING is non-nil while filtering parameters to save a frameset,
nil while the filtering is done to restore it."
  (let ((filtered nil))
    (dolist (current parameters)
640 641
      ;; When saving, the parameter alist is temporary, so modifying it
      ;; is not a problem.  When restoring, the parameter alist is part
642 643
      ;; of a frameset, so we must copy parameters to avoid inadvertent
      ;; modifications.
644 645
      (pcase (cdr (assq (car current) filter-alist))
	(`nil
646
	 (push (if saving current (copy-tree current)) filtered))
647
	(:never
648 649
	 nil)
	(:restore
650
	 (unless saving (push (copy-tree current) filtered)))
651
	(:save
652 653
	 (when saving (push current filtered)))
	((or `(,fun . ,args) (and fun (pred fboundp)))
654 655 656 657
	 (let* ((this (apply fun current filtered parameters saving args))
		(val (if (eq this t) current this)))
	   (when val
	     (push (if saving val (copy-tree val)) filtered))))
658 659 660 661 662 663 664 665 666 667 668 669
	(other
	 (delay-warning 'frameset (format "Unknown filter %S" other) :error))))
    ;; Set the display parameter after filtering, so that filter functions
    ;; have access to its original value.
    (when frameset--target-display
      (let ((display (assq 'display filtered)))
	(if display
	    (setcdr display (cdr frameset--target-display))
	  (push frameset--target-display filtered))))
    filtered))


670
;; Frame ids
671 672

(defun frameset--set-id (frame)
673
  "Set FRAME's id if not yet set.
674
Internal use only."
675
  (unless (frame-parameter frame 'frameset--id)
676
    (set-frame-parameter frame
677
			 'frameset--id
678 679 680
			 (mapconcat (lambda (n) (format "%04X" n))
				    (cl-loop repeat 4 collect (random 65536))
				    "-"))))
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696
;;;###autoload
(defun frameset-frame-id (frame)
  "Return the frame id of FRAME, if it has one; else, return nil.
A frame id is a string that uniquely identifies a frame.
It is persistent across `frameset-save' / `frameset-restore'
invocations, and once assigned is never changed unless the same
frame is duplicated (via `frameset-restore'), in which case the
newest frame keeps the id and the old frame's is set to nil."
  (frame-parameter frame 'frameset--id))

;;;###autoload
(defun frameset-frame-id-equal-p (frame id)
  "Return non-nil if FRAME's id matches ID."
  (string= (frameset-frame-id frame) id))

;;;###autoload
697
(defun frameset-frame-with-id (id &optional frame-list)
698 699 700 701 702 703 704 705 706 707
  "Return the live frame with id ID, if exists; else nil.
If FRAME-LIST is a list of frames, check these frames only.
If nil, check all live frames."
  (cl-find-if (lambda (f)
		(and (frame-live-p f)
		     (frameset-frame-id-equal-p f id)))
	      (or frame-list (frame-list))))


;; Saving framesets
708

709
(defun frameset--record-minibuffer-relationships (frame-list)
710
  "Process FRAME-LIST and record minibuffer relationships.
711
FRAME-LIST is a list of frames.  Internal use only."
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
  ;; Record frames with their own minibuffer
  (dolist (frame (minibuffer-frame-list))
    (when (memq frame frame-list)
      (frameset--set-id frame)
      ;; For minibuffer-owning frames, frameset--mini is a cons
      ;; (t . DEFAULT?), where DEFAULT? is a boolean indicating whether
      ;; the frame is the one pointed out by `default-minibuffer-frame'.
      (set-frame-parameter frame
			   'frameset--mini
			   (cons t (eq frame default-minibuffer-frame)))))
  ;; Now link minibufferless frames with their minibuffer frames
  (dolist (frame frame-list)
    (unless (frame-parameter frame 'frameset--mini)
      (frameset--set-id frame)
      (let* ((mb-frame (window-frame (minibuffer-window frame)))
727
	     (id (and mb-frame (frameset-frame-id mb-frame))))
728
	(if (null id)
729
	    (error "Minibuffer frame %S for %S is not being saved" mb-frame frame)
730
	  ;; For minibufferless frames, frameset--mini is a cons
731 732
	  ;; (nil . FRAME-ID), where FRAME-ID is the frameset--id
	  ;; of the frame containing its minibuffer window.
733 734 735 736
	  (set-frame-parameter frame
			       'frameset--mini
			       (cons nil id)))))))

737
;;;###autoload
738 739 740 741
(cl-defun frameset-save (frame-list
			 &key app name description
			      filters predicate properties)
  "Return a frameset for FRAME-LIST, a list of frames.
742 743
Dead frames and non-frame objects are silently removed from the list.
If nil, FRAME-LIST defaults to the output of `frame-list' (all live frames).
744 745 746 747
APP, NAME and DESCRIPTION are optional data; see the docstring of the
`frameset' defstruct for details.
FILTERS is an alist of parameter filters; if nil, the value of the variable
`frameset-filter-alist' is used instead.
748
PREDICATE is a predicate function, which must return non-nil for frames that
749
should be saved; if PREDICATE is nil, all frames from FRAME-LIST are saved.
750
PROPERTIES is a user-defined property list to add to the frameset."
751 752 753 754
  (let* ((list (or (copy-sequence frame-list) (frame-list)))
	 (frames (cl-delete-if-not #'frame-live-p
				   (if predicate
				       (cl-delete-if-not predicate list)
755 756
				     list)))
	 fs)
757
    (frameset--record-minibuffer-relationships frames)
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773
    (setq fs (make-frameset
	      :app app
	      :name name
	      :description description
	      :properties properties
	      :states (mapcar
		       (lambda (frame)
			 (cons
			  (frameset-filter-params (frame-parameters frame)
						  (or filters
						      frameset-filter-alist)
						  t)
			  (window-state-get (frame-root-window frame) t)))
		       frames)))
    (cl-assert (frameset-valid-p fs))
    fs))
774 775 776 777 778


;; Restoring framesets

(defvar frameset--reuse-list nil
779 780 781
  "The list of frames potentially reusable.
Its value is only meaningful during execution of `frameset-restore'.
Internal use only.")
782

783 784 785 786 787 788 789 790 791 792
(defun frameset-compute-pos (value left/top right/bottom)
  "Return an absolute positioning value for a frame.
VALUE is the value of a positional frame parameter (`left' or `top').
If VALUE is relative to the screen edges (like (+ -35) or (-200), it is
converted to absolute by adding it to the corresponding edge; if it is
an absolute position, it is returned unmodified.
LEFT/TOP and RIGHT/BOTTOM indicate the dimensions of the screen in
pixels along the relevant direction: either the position of the left
and right edges for a `left' positional parameter, or the position of
the top and bottom edges for a `top' parameter."
793 794 795 796 797
  (pcase value
    (`(+ ,val) (+ left/top val))
    (`(- ,val) (+ right/bottom val))
    (val val)))

798
(defun frameset-move-onscreen (frame force-onscreen)
799 800 801 802
  "If FRAME is offscreen, move it back onscreen and, if necessary, resize it.
For the description of FORCE-ONSCREEN, see `frameset-restore'.
When forced onscreen, frames wider than the monitor's workarea are converted
to fullwidth, and frames taller than the workarea are converted to fullheight.
803
NOTE: This only works for non-iconified frames."
804
  (pcase-let* ((`(,left ,top ,width ,height) (cl-cdadr (frame-monitor-attributes frame)))
805 806
	       (right (+ left width -1))
	       (bottom (+ top height -1))
807 808
	       (fr-left (frameset-compute-pos (frame-parameter frame 'left) left right))
	       (fr-top (frameset-compute-pos (frame-parameter frame 'top) top bottom))
809 810
	       (ch-width (frame-char-width frame))
	       (ch-height (frame-char-height frame))
811 812 813 814
	       (fr-width (max (frame-pixel-width frame) (* ch-width (frame-width frame))))
	       (fr-height (max (frame-pixel-height frame) (* ch-height (frame-height frame))))
	       (fr-right (+ fr-left fr-width -1))
	       (fr-bottom (+ fr-top fr-height -1)))
815
    (when (pcase force-onscreen
816 817 818 819 820 821
	    ;; A predicate.
	    ((pred functionp)
	     (funcall force-onscreen
		      frame
		      (list fr-left fr-top fr-width fr-height)
		      (list left top width height)))
822
	    ;; Any corner is outside the screen.
823
	    (:all (or (< fr-bottom top)  (> fr-bottom bottom)
824 825
		      (< fr-left   left) (> fr-left   right)
		      (< fr-right  left) (> fr-right  right)
826
		      (< fr-top    top)  (> fr-top    bottom)))
827
	    ;; Displaced to the left, right, above or below the screen.
828
	    (`t   (or (> fr-left   right)
829
		      (< fr-right  left)
830
		      (> fr-top    bottom)
831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
		      (< fr-bottom top)))
	    ;; Fully inside, no need to do anything.
	    (_ nil))
      (let ((fullwidth (> fr-width width))
	    (fullheight (> fr-height height))
	    (params nil))
	;; Position frame horizontally.
	(cond (fullwidth
	       (push `(left . ,left) params))
	      ((> fr-right right)
	       (push `(left . ,(+ left (- width fr-width))) params))
	      ((< fr-left left)
	       (push `(left . ,left) params)))
	;; Position frame vertically.
	(cond (fullheight
	       (push `(top . ,top) params))
	      ((> fr-bottom bottom)
	       (push `(top . ,(+ top (- height fr-height))) params))
	      ((< fr-top top)
	       (push `(top . ,top) params)))
	;; Compute fullscreen state, if required.
	(when (or fullwidth fullheight)
	  (push (cons 'fullscreen
		      (cond ((not fullwidth) 'fullheight)
			    ((not fullheight) 'fullwidth)
			    (t 'maximized)))
		params))
	;; Finally, move the frame back onscreen.
	(when params
	  (modify-frame-parameters frame params))))))

862
(defun frameset--find-frame-if (predicate display &rest args)
863 864 865 866 867 868 869 870 871 872
  "Find a frame in `frameset--reuse-list' satisfying PREDICATE.
Look through available frames whose display property matches DISPLAY
and return the first one for which (PREDICATE frame ARGS) returns t.
If PREDICATE is nil, it is always satisfied.  Internal use only."
  (cl-find-if (lambda (frame)
		(and (equal (frame-parameter frame 'display) display)
		     (or (null predicate)
			 (apply predicate frame args))))
	      frameset--reuse-list))

873 874 875
(defun frameset--reuse-frame (display parameters)
  "Return an existing frame to reuse, or nil if none found.
DISPLAY is the display where the frame will be shown, and PARAMETERS
876
is the parameter alist of the frame being restored.  Internal use only."
877 878 879 880 881 882 883 884 885 886 887 888
  (let ((frame nil)
	mini)
    ;; There are no fancy heuristics there.  We could implement some
    ;; based on frame size and/or position, etc., but it is not clear
    ;; that any "gain" (in the sense of reduced flickering, etc.) is
    ;; worth the added complexity.  In fact, the code below mainly
    ;; tries to work nicely when M-x desktop-read is used after a
    ;; desktop session has already been loaded.  The other main use
    ;; case, which is the initial desktop-read upon starting Emacs,
    ;; will usually have only one frame, and should already work.
    (cond ((null display)
	   ;; When the target is tty, every existing frame is reusable.
889 890
	   (setq frame (frameset--find-frame-if nil display)))
	  ((car (setq mini (cdr (assq 'frameset--mini parameters))))
891 892 893
	   ;; If the frame has its own minibuffer, let's see whether
	   ;; that frame has already been loaded (which can happen after
	   ;; M-x desktop-read).
894
	   (setq frame (frameset--find-frame-if
895
			(lambda (f id)
896
			  (frameset-frame-id-equal-p f id))
897
			display (cdr (assq 'frameset--id parameters))))
898 899
	   ;; If it has not been loaded, and it is not a minibuffer-only frame,
	   ;; let's look for an existing non-minibuffer-only frame to reuse.
900 901
	   (unless (or frame (eq (cdr (assq 'minibuffer parameters)) 'only))
	     (setq frame (frameset--find-frame-if
902 903 904 905 906 907 908 909 910
			  (lambda (f)
			    (let ((w (frame-parameter f 'minibuffer)))
			      (and (window-live-p w)
				   (window-minibuffer-p w)
				   (eq (window-frame w) f))))
			  display))))
	  (mini
	   ;; For minibufferless frames, check whether they already exist,
	   ;; and that they are linked to the right minibuffer frame.
911
	   (setq frame (frameset--find-frame-if
912
			(lambda (f id mini-id)
913 914 915 916
			  (and (frameset-frame-id-equal-p f id)
			       (frameset-frame-id-equal-p (window-frame
							   (minibuffer-window f))
							  mini-id)))
917
			display (cdr (assq 'frameset--id parameters)) (cdr mini))))
918 919
	  (t
	   ;; Default to just finding a frame in the same display.
920
	   (setq frame (frameset--find-frame-if nil display))))
921 922 923 924 925
    ;; If found, remove from the list.
    (when frame
      (setq frameset--reuse-list (delq frame frameset--reuse-list)))
    frame))

926 927
(defun frameset--initial-params (parameters)
  "Return a list of PARAMETERS that must be set when creating the frame.
928
Setting position and size parameters as soon as possible helps reducing
929 930
flickering; other parameters, like `minibuffer' and `border-width', can
not be changed once the frame has been created.  Internal use only."
931
  (cl-loop for param in '(left top with height border-width minibuffer)
932
	   collect (assq param parameters)))
933

934
(defun frameset--restore-frame (parameters window-state filters force-onscreen)
935 936
  "Set up and return a frame according to its saved state.
That means either reusing an existing frame or creating one anew.
937
PARAMETERS is the frame's parameter alist; WINDOW-STATE is its window state.
938
For the meaning of FILTERS and FORCE-ONSCREEN, see `frameset-restore'.
939
Internal use only."
940 941 942
  (let* ((fullscreen (cdr (assq 'fullscreen parameters)))
	 (lines (assq 'tool-bar-lines parameters))
	 (filtered-cfg (frameset-filter-params parameters filters nil))
943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976
	 (display (cdr (assq 'display filtered-cfg))) ;; post-filtering
	 alt-cfg frame)

    ;; This works around bug#14795 (or feature#14795, if not a bug :-)
    (setq filtered-cfg (assq-delete-all 'tool-bar-lines filtered-cfg))
    (push '(tool-bar-lines . 0) filtered-cfg)

    (when fullscreen
      ;; Currently Emacs has the limitation that it does not record the size
      ;; and position of a frame before maximizing it, so we cannot save &
      ;; restore that info.  Instead, when restoring, we resort to creating
      ;; invisible "fullscreen" frames of default size and then maximizing them
      ;; (and making them visible) which at least is somewhat user-friendly
      ;; when these frames are later de-maximized.
      (let ((width (and (eq fullscreen 'fullheight) (cdr (assq 'width filtered-cfg))))
	    (height (and (eq fullscreen 'fullwidth) (cdr (assq 'height filtered-cfg))))
	    (visible (assq 'visibility filtered-cfg)))
	(setq filtered-cfg (cl-delete-if (lambda (p)
					   (memq p '(visibility fullscreen width height)))
					 filtered-cfg :key #'car))
	(when width
	  (setq filtered-cfg (append `((user-size . t) (width . ,width))
				     filtered-cfg)))
	(when height
	  (setq filtered-cfg (append `((user-size . t) (height . ,height))
				     filtered-cfg)))
	;; These are parameters to apply after creating/setting the frame.
	(push visible alt-cfg)
	(push (cons 'fullscreen fullscreen) alt-cfg)))

    ;; Time to find or create a frame an apply the big bunch of parameters.
    ;; If a frame needs to be created and it falls partially or fully offscreen,
    ;; sometimes it gets "pushed back" onscreen; however, moving it afterwards is
    ;; allowed.  So we create the frame as invisible and then reapply the full
977
    ;; parameter alist (including position and size parameters).
978 979 980 981
    (setq frame (or (and frameset--reuse-list
			 (frameset--reuse-frame display filtered-cfg))
		    (make-frame-on-display display
					   (cons '(visibility)
982
						 (frameset--initial-params filtered-cfg)))))
983 984 985 986 987 988 989 990 991 992 993
    (modify-frame-parameters frame
			     (if (eq (frame-parameter frame 'fullscreen) fullscreen)
				 ;; Workaround for bug#14949
				 (assq-delete-all 'fullscreen filtered-cfg)
			       filtered-cfg))

    ;; If requested, force frames to be onscreen.
    (when (and force-onscreen
	       ;; FIXME: iconified frames should be checked too,
	       ;; but it is impossible without deiconifying them.
	       (not (eq (frame-parameter frame 'visibility) 'icon)))
994
      (frameset-move-onscreen frame force-onscreen))
995 996 997 998 999

    ;; Let's give the finishing touches (visibility, tool-bar, maximization).
    (when lines (push lines alt-cfg))
    (when alt-cfg (modify-frame-parameters frame alt-cfg))
    ;; Now restore window state.
1000
    (window-state-put window-state (frame-root-window frame) 'safe)
1001 1002
    frame))

1003
(defun frameset--minibufferless-last-p (state1 state2)
1004
  "Predicate to sort frame states in an order suitable for creating frames.
1005 1006
It sorts minibuffer-owning frames before minibufferless ones.
Internal use only."
1007 1008 1009 1010 1011 1012 1013 1014
  (pcase-let ((`(,hasmini1 ,id-def1) (assq 'frameset--mini (car state1)))
	      (`(,hasmini2 ,id-def2) (assq 'frameset--mini (car state2))))
    (cond ((eq id-def1 t) t)
	  ((eq id-def2 t) nil)
	  ((not (eq hasmini1 hasmini2)) (eq hasmini1 t))
	  ((eq hasmini1 nil) (string< id-def1 id-def2))
	  (t t))))

1015
(defun frameset-keep-original-display-p (force-display)
1016 1017
  "True if saved frames' displays should be honored.
For the meaning of FORCE-DISPLAY, see `frameset-restore'."
1018
  (cond ((daemonp) t)
1019
	((eq system-type 'windows-nt) nil) ;; Does ns support more than one display?
1020 1021
	(t (not force-display))))

1022 1023
(defun frameset-minibufferless-first-p (frame1 _frame2)
  "Predicate to sort minibufferless frames before other frames."
1024 1025
  (not (frame-parameter frame1 'minibuffer)))

1026
;;;###autoload
1027
(cl-defun frameset-restore (frameset
1028
			    &key predicate filters reuse-frames
1029
				 force-display force-onscreen)
1030 1031
  "Restore a FRAMESET into the current display(s).

1032 1033 1034 1035 1036 1037 1038 1039
PREDICATE is a function called with two arguments, the parameter alist
and the window-state of the frame being restored, in that order (see
the docstring of the `frameset' defstruct for additional details).
If PREDICATE returns nil, the frame described by that parameter alist
and window-state is not restored.

FILTERS is an alist of parameter filters; if nil, the value of
`frameset-filter-alist' is used instead.
1040

1041
REUSE-FRAMES selects the policy to use to reuse frames when restoring:
1042 1043 1044 1045
  t        Reuse existing frames if possible, and delete those not reused.
  nil      Restore frameset in new frames and delete existing frames.
  :keep    Restore frameset in new frames and keep the existing ones.
  LIST     A list of frames to reuse; only these are reused (if possible).
1046 1047
	     Remaining frames in this list are deleted; other frames not
	     included on the list are left untouched.
1048 1049

FORCE-DISPLAY can be:
1050 1051
  t        Frames are restored in the current display.
  nil      Frames are restored, if possible, in their original displays.
1052
  :delete  Frames in other displays are deleted instead of restored.
1053
  PRED     A function called with two arguments, the parameter alist and
1054 1055 1056
	     the window state (in that order).  It must return t, nil or
	     `:delete', as above but affecting only the frame that will
	     be created from that parameter alist.
1057 1058

FORCE-ONSCREEN can be:
1059 1060 1061 1062
  t        Force onscreen only those frames that are fully offscreen.
  nil      Do not force any frame back onscreen.
  :all     Force onscreen any frame fully or partially offscreen.
  PRED     A function called with three arguments,
1063 1064
	   - the live frame just restored,
	   - a list (LEFT TOP WIDTH HEIGHT), describing the frame,
1065 1066
	   - a list (LEFT TOP WIDTH HEIGHT), describing the workarea.
	   It must return non-nil to force the frame onscreen, nil otherwise.
1067 1068

Note the timing and scope of the operations described above: REUSE-FRAMES
1069 1070
affects existing frames; PREDICATE, FILTERS and FORCE-DISPLAY affect the frame
being restored before that happens; and FORCE-ONSCREEN affects the frame once
1071
it has been restored.
1072

1073
All keyword parameters default to nil."
1074

1075
  (cl-assert (frameset-valid-p frameset))
1076

1077
  (let (other-frames)
1078 1079 1080 1081

    ;; frameset--reuse-list is a list of frames potentially reusable.  Later we
    ;; will decide which ones can be reused, and how to deal with any leftover.
    (pcase reuse-frames
1082
      ((or `nil `:keep)
1083 1084 1085 1086 1087
       (setq frameset--reuse-list nil
	     other-frames (frame-list)))
      ((pred consp)
       (setq frameset--reuse-list (copy-sequence reuse-frames)
	     other-frames (cl-delete-if (lambda (frame)
1088 1089
					  (memq frame frameset--reuse-list))
					(frame-list))))
1090 1091 1092 1093 1094 1095 1096
      (_
       (setq frameset--reuse-list (frame-list)
	     other-frames nil)))

    ;; Sort saved states to guarantee that minibufferless frames will be created
    ;; after the frames that contain their minibuffer windows.
    (dolist (state (sort (copy-sequence (frameset-states frameset))
1097
			 #'frameset--minibufferless-last-p))
1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165
      (pcase-let ((`(,frame-cfg . ,window-cfg) state))
	(when (or (null predicate) (funcall predicate frame-cfg window-cfg))
	  (condition-case-unless-debug err
	      (let* ((d-mini (cdr (assq 'frameset--mini frame-cfg)))
		     (mb-id (cdr d-mini))
		     (default (and (booleanp mb-id) mb-id))
		     (force-display (if (functionp force-display)
					(funcall force-display frame-cfg window-cfg)
				      force-display))
		     frame to-tty)
		;; Only set target if forcing displays and the target display is different.
		(cond ((frameset-keep-original-display-p force-display)
		       (setq frameset--target-display nil))
		      ((eq (frame-parameter nil 'display) (cdr (assq 'display frame-cfg)))
		       (setq frameset--target-display nil))
		      (t
		       (setq frameset--target-display (cons 'display
							    (frame-parameter nil 'display))
			     to-tty (null (cdr frameset--target-display)))))
		;; Time to restore frames and set up their minibuffers as they were.
		;; We only skip a frame (thus deleting it) if either:
		;; - we're switching displays, and the user chose the option to delete, or
		;; - we're switching to tty, and the frame to restore is minibuffer-only.
		(unless (and frameset--target-display
			     (or (eq force-display :delete)
				 (and to-tty
				      (eq (cdr (assq 'minibuffer frame-cfg)) 'only))))
		  ;; If keeping non-reusable frames, and the frameset--id of one of them
		  ;; matches the id of a frame being restored (because, for example, the
		  ;; frameset has already been read in the same session), remove the
		  ;; frameset--id from the non-reusable frame, which is not useful anymore.
		  (when (and other-frames
			     (or (eq reuse-frames :keep) (consp reuse-frames)))
		    (let ((dup (frameset-frame-with-id (cdr (assq 'frameset--id frame-cfg))
						       other-frames)))
		      (when dup
			(set-frame-parameter dup 'frameset--id nil))))
		  ;; Restore minibuffers.  Some of this stuff could be done in a filter
		  ;; function, but it would be messy because restoring minibuffers affects
		  ;; global state; it's best to do it here than add a bunch of global
		  ;; variables to pass info back-and-forth to/from the filter function.
		  (cond
		   ((null d-mini)) ;; No frameset--mini.  Process as normal frame.
		   (to-tty) ;; Ignore minibuffer stuff and process as normal frame.
		   ((car d-mini) ;; Frame has minibuffer (or it is minibuffer-only).
		    (when (eq (cdr (assq 'minibuffer frame-cfg)) 'only)
		      (setq frame-cfg (append '((tool-bar-lines . 0) (menu-bar-lines . 0))
					      frame-cfg))))
		   (t ;; Frame depends on other frame's minibuffer window.
		    (let* ((mb-frame (or (frameset-frame-with-id mb-id)
					 (error "Minibuffer frame %S not found" mb-id)))
			   (mb-param (assq 'minibuffer frame-cfg))
			   (mb-window (minibuffer-window mb-frame)))
		      (unless (and (window-live-p mb-window)
				   (window-minibuffer-p mb-window))
			(error "Not a minibuffer window %s" mb-window))
		      (if mb-param
			  (setcdr mb-param mb-window)
			(push (cons 'minibuffer mb-window) frame-cfg)))))
		  ;; OK, we're ready at last to create (or reuse) a frame and
		  ;; restore the window config.
		  (setq frame (frameset--restore-frame frame-cfg window-cfg
						       (or filters frameset-filter-alist)
						       force-onscreen))
		  ;; Set default-minibuffer if required.
		  (when default (setq default-minibuffer-frame frame))))
	    (error
	     (delay-warning 'frameset (error-message-string err) :error))))))
1166 1167 1168 1169 1170 1171

    ;; In case we try to delete the initial frame, we want to make sure that
    ;; other frames are already visible (discussed in thread for bug#14841).
    (sit-for 0 t)

    ;; Delete remaining frames, but do not fail if some resist being deleted.
1172
    (unless (eq reuse-frames :keep)
1173 1174
      (dolist (frame (sort (nconc (if (listp reuse-frames) nil other-frames)
				  frameset--reuse-list)
1175 1176 1177 1178
			   ;; Minibufferless frames must go first to avoid
			   ;; errors when attempting to delete a frame whose
			   ;; minibuffer window is used by another frame.
			   #'frameset-minibufferless-first-p))
1179 1180 1181 1182
	(condition-case err
	    (delete-frame frame)
	  (error
	   (delay-warning 'frameset (error-message-string err))))))
1183 1184
    (setq frameset--reuse-list nil
	  frameset--target-display nil)
1185 1186 1187 1188 1189

    ;; Make sure there's at least one visible frame.
    (unless (or (daemonp) (visible-frame-list))
      (make-frame-visible (car (frame-list))))))

1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224

;; Register support

(defun frameset--jump-to-register (data)
  "Restore frameset from DATA stored in register.
Called from `jump-to-register'.  Internal use only."
  (let* ((delete (and current-prefix-arg t))
	 (iconify-list (if delete nil (frame-list))))
    (frameset-restore (aref data 0)
		      :filters frameset-session-filter-alist
		      :reuse-frames (if delete t :keep))
    (mapc #'iconify-frame iconify-list)
    (let ((frame (frameset-frame-with-id (aref data 1))))
      (when frame
	(select-frame-set-input-focus frame)
	(goto-char (aref data 2))))))

;;;###autoload
(defun frameset-to-register (register &optional _arg)
  "Store the current frameset in register REGISTER.
Use \\[jump-to-register] to restore the frameset.
Argument is a character, naming the register."
  (interactive "cFrameset to register: \nP")
  (set-register register
		(registerv-make
		 (vector (frameset-save nil
					:app 'register
					:filters frameset-session-filter-alist)
			 ;; frameset-save does not include the value of point
			 ;; in the current buffer, so record that separately.
			 (frameset-frame-id nil)
			 (point-marker))
		 :print-func (lambda (_data) (princ "a frameset."))
		 :jump-func #'frameset--jump-to-register)))

1225 1226 1227
(provide 'frameset)

;;; frameset.el ends here