Subject: Re: bug#51883: 29.0.50; Command to get accidentally deleted frames back

From: Gregory Heytings <[email-masked]>
Date: Tue, 16 Nov 2021 15:17:37 +0000

Patch attached.

From 7d5ffe8af8348bf1b3ff888816f2517ee4aa345e Mon Sep 17 00:00:00 2001
From: Gregory Heytings <[email-masked]>
Date: Tue, 16 Nov 2021 15:10:41 +0000
Subject: [PATCH] Undelete deleted frames.

* lisp/frame.el (undelete-frame): New command.
(undeleted-frame--save-deleted-frame): New auxiliary function.
(undelete-frame--deleted-frames): New auxiliary variable.
(make-frame-command): Add a prefix argument, and call the new
command.

* lisp/menu-bar.el (menu-bar-file-menu): Add an entry for the
new command.

* doc/emacs/frames.tex (Creating Frames, Frame Commands): Document
the new command.

* etc/NEWS: Document the new command.

See bug#51883.
---
 doc/emacs/frames.texi | 11 ++++++---
 etc/NEWS              | 10 ++++++++
 lisp/frame.el         | 56 +++++++++++++++++++++++++++++++++++++++----
 lisp/menu-bar.el      |  4 ++++
 4 files changed, 73 insertions(+), 8 deletions(-)

diff --git a/doc/emacs/frames.texi b/doc/emacs/frames.texi
index c14ada2957..f5ede6af5e 100644
--- a/doc/emacs/frames.texi
+++ b/doc/emacs/frames.texi
@@ -452,8 +452,11 @@ Creating Frames
 @item C-x 5 2
 @kindex C-x 5 2
 @findex make-frame-command
-Create a new frame using the default frame parameters
-(@code{make-frame-command}).
+Create a new frame using the default frame parameters, or, with a prefix
+argument, undelete one of the 16 most recently deleted frames
+(@code{make-frame-command}).  A prefix argument undeletes the last deleted
+frame, a numerical prefix argument between 0 and 15 undeletes the
+corresponding deleted frame, where 0 is the most recently deleted frame.
 
 @item C-x 5 c
 @kindex C-x 5 c
@@ -510,7 +513,9 @@ Frame Commands
 @kindex C-x 5 0
 @findex delete-frame
 Delete the selected frame (@code{delete-frame}).  This signals an
-error if there is only one frame.
+error if there is only one frame.  The 16 most recently deleted frames
+can be undeleted with the @kbd{C-x 5 2} command, when it is used with
+a prefix argument.
 
 @item C-z
 @kindex C-z @r{(X windows)}
diff --git a/etc/NEWS b/etc/NEWS
index 312fc18f4f..ab47f8cf5b 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -130,6 +130,16 @@ For example, a 'display-buffer-alist' entry of
 will make the body of the chosen window 40 columns wide.  For the
 height use 'window-height' in combination with 'body-lines'.
 
+---
+** Frames
+
++++
+*** With a prefix argument, the key 'C-x 5 2' undeletes deleted frames.
+The 16 most recently deleted frames can be undeleted.  A prefix argument
+undeletes the last deleted frame, a numerical prefix argument between 0
+and 15 undeletes the corresponding deleted frame, where 0 is the most
+recently deleted frame.
+
 ** Better detection of text suspiciously reordered on display.
 The function 'bidi-find-overridden-directionality' has been extended
 to detect reordering effects produced by embeddings and isolates
diff --git a/lisp/frame.el b/lisp/frame.el
index 2c73737a54..2085397b30 100644
--- a/lisp/frame.el
+++ b/lisp/frame.el
@@ -775,16 +775,23 @@ close-display-connection
       (mapc #'delete-frame frames)
       (x-close-connection display))))
 
-(defun make-frame-command ()
+(defun make-frame-command (&optional arg)
   "Make a new frame, on the same terminal as the selected frame.
 If the terminal is a text-only terminal, this also selects the
 new frame.
 
+With a prefix argument ARG, undelete the most recently deleted
+frame.
+With a numerical prefix argument ARG between 0 and 15, undelete
+the ARGth deleted frame, where 0 is most recently deleted frame.
+
 When called from Lisp, returns the new frame."
-  (interactive)
-  (if (display-graphic-p)
-      (make-frame)
-    (select-frame (make-frame))))
+  (interactive "P")
+  (if arg
+      (undelete-frame arg)
+    (if (display-graphic-p)
+        (make-frame)
+      (select-frame (make-frame)))))
 
 (defun clone-frame (&optional frame no-windows)
   "Make a new frame with the same parameters and windows as FRAME.
@@ -2484,6 +2491,45 @@ delete-other-frames
         (if iconify (iconify-frame this) (delete-frame this)))
       (setq this next))))
 
+(eval-when-compile (require 'frameset))
+
+(defvar undelete-frame--deleted-frames nil
+  "Internal variable used by `undelete-frame--save-deleted-frame'.")
+
+(defun undeleted-frame--save-deleted-frame (frame)
+  "Save the configuration of frames deleted with `delete-frame'.
+Only the 16 most recently deleted frames are saved."
+  (when (frame-live-p frame)
+    (setq undelete-frame--deleted-frames
+          (cons (cons
+                 (display-graphic-p)
+                 (frameset-save (list frame)))
+                undelete-frame--deleted-frames))
+    (if (> (length undelete-frame--deleted-frames) 16)
+        (setq undelete-frame--deleted-frames
+              (butlast undelete-frame--deleted-frames)))))
+
+(add-hook 'delete-frame-functions #'undeleted-frame--save-deleted-frame)
+
+(defun undelete-frame (&optional arg)
+  "Undelete a frame deleted with `delete-frame'.
+When ARG is nil or a list, the last deleted frame is undeleted.
+When ARG is a number between 0 and 15, the ARGth deleted frame is
+undeleted.
+When called from Lisp, returns the new frame."
+  (interactive "P")
+  (let* ((frames (frame-list))
+         (n (if (listp arg) 0 arg))
+         (frameset (nth n undelete-frame--deleted-frames)))
+    (when (and frameset
+               (eq (display-graphic-p) (car frameset)))
+      (setq undelete-frame--deleted-frames
+            (delq frameset undelete-frame--deleted-frames))
+      (frameset-restore (cdr frameset))
+      (let ((frame (car (seq-difference (frame-list) frames))))
+        (when frame
+          (select-frame-set-input-focus frame)
+          frame)))))
 
 ;;; Window dividers.
 (defgroup window-divider nil
diff --git a/lisp/menu-bar.el b/lisp/menu-bar.el
index 1a81f1a3d0..a1e0195bb5 100644
--- a/lisp/menu-bar.el
+++ b/lisp/menu-bar.el
@@ -109,6 +109,10 @@ menu-bar-file-menu
       (bindings--define-key menu [separator-tab]
         menu-bar-separator))
 
+    (bindings--define-key menu [undelete-last-deleted-frame]
+      '(menu-item "Undelete Frame" undelete-frame
+                  :help "Undelete last deleted frame"))
+
     ;; Don't use delete-frame as event name because that is a special
     ;; event.
     (bindings--define-key menu [delete-this-frame]
-- 
2.33.0