From 785059a1f7ab0ab272b2463d7a7958746ca8fe80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Marks?= Date: Tue, 13 Jan 2026 09:29:44 +0100 Subject: [PATCH] Add frame identifiers (bug#80138) A unique frame id is assigned to a new or cloned frame, and reused on an undeleted frame. The id facilitates unambiguous identification among frames that share identical names or titles, deleted frames where a live frame object no longer exists that we can resurrect by id, for example via 'tab-bar-undo-close-tab'. It also aids debugging at the C level using the frame struct member id. Rewrite 'clone-frame' and 'undelete-frame' to not let bind variables that 'make-frame' uses to avoid conflicts with nested 'make-frame' calls, for example via 'after-make-frame-functions'. * lisp/frame.el (clone-frame, undelete-frame): Use 'frame--purify-parameters' to supply parameters explicitly. (undelete-frame--save-deleted-frame): Save frame id for restoration. (undelete-frame): Restore frame id. (frame--purify-parameters): New defun. (make-frame): Assign a new id for a new or cloned frame, reuse for undeleted frame. * src/frame.h (struct frame): Add id member. (frame_next_id): New extern. * src/frame.c (frame_next_id): New global counter. (frame_set_id, frame_set_id_from_params): New function. (Fframe_id): New DEFUN. (syms_of_frame ): New defsubr. (syms_of_frame ): New DEFSYM. (syms_of_frame ): Add 'Qinternal_id'. * src/androidfns.c (Fx_create_frame): * src/haikufns.c (Fx_create_frame): * src/nsfns.m (Fx_create_frame): * src/pgtkfns.c (Fx_create_frame): * src/w32fns.c (Fx_create_frame): * src/xfns.c (Fx_create_frame): Call 'frame_set_id_from_params'. * doc/lispref/frames.texi: Add documentation. * etc/NEWS: Announce frame id. --- doc/lispref/frames.texi | 18 +++++++++ etc/NEWS | 8 ++++ lisp/frame.el | 60 ++++++++++++++++++++++-------- src/androidfns.c | 2 + src/frame.c | 82 +++++++++++++++++++++++++++++++++++++++++ src/frame.h | 7 ++++ src/haikufns.c | 2 + src/nsfns.m | 2 + src/pgtkfns.c | 2 + src/w32fns.c | 2 + src/xfns.c | 2 + 11 files changed, 171 insertions(+), 16 deletions(-) diff --git a/doc/lispref/frames.texi b/doc/lispref/frames.texi index 303c047023b..5bf0bfc8c10 100644 --- a/doc/lispref/frames.texi +++ b/doc/lispref/frames.texi @@ -109,6 +109,24 @@ must be a root frame, which means it cannot be a child frame itself descending from it. @end defun +@defun frame-id &optional frame +This function returns the unique identifier of a frame, an integer, +assigned to @var{frame}. If @var{frame} is @code{nil} or unspecified, +it defaults to the selected frame (@pxref{Input Focus}). This can be +used to unambiguously identify a frame in a context where you do not or +cannot use a frame object. + +A frame undeleted using @command{undelete-frame} will retain its +identifier. A frame cloned using @command{clone-frame} will not retain +its original identifier. @xref{Frame Commands,,,emacs, the Emacs +Manual}. + +Frame identifiers are not persisted using the desktop library +(@pxref{Desktop Save Mode}), @command{frameset-to-register}, or +@code{frameset-save}, and each of their restored frames will bear a new +unique id. +@end defun + @menu * Creating Frames:: Creating additional frames. * Multiple Terminals:: Displaying on several different devices. diff --git a/etc/NEWS b/etc/NEWS index 93d40a9d384..f1f7deec4e7 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -500,6 +500,14 @@ These are useful if you need to detect a cloned frame or undeleted frame in hooks like 'after-make-frame-functions' and 'server-after-make-frame-hook'. +--- +*** Frames now have unique ids and the new function 'frame-id'. +Each non-tooltip frame is assigned a unique integer id. This allows you +to unambiguously identify frames even if they share the same name or +title. When 'undelete-frame-mode' is enabled, each deleted frame's id +is stored for resurrection. The function 'frame-id' returns a frame's +id (in C, use the frame struct member id). + ** Mode Line +++ diff --git a/lisp/frame.el b/lisp/frame.el index d1b70a78c66..5ccfac2cadc 100644 --- a/lisp/frame.el +++ b/lisp/frame.el @@ -951,24 +951,24 @@ and lines for the clone. FRAME defaults to the selected frame. The frame is created on the same terminal as FRAME. If the terminal is a text-only terminal then -also select the new frame." +also select the new frame. + +A cloned frame is assigned a new frame ID. See `frame-id'." (interactive (list (selected-frame) current-prefix-arg)) (let* ((frame (or frame (selected-frame))) (windows (unless no-windows (window-state-get (frame-root-window frame)))) - (default-frame-alist + (parameters (append `((cloned-from . ,frame)) - (seq-remove (lambda (elem) - (memq (car elem) frame-internal-parameters)) - (frame-parameters frame)))) + (frame--purify-parameters (frame-parameters frame)))) new-frame) (when (and frame-resize-pixelwise (display-graphic-p frame)) (push (cons 'width (cons 'text-pixels (frame-text-width frame))) - default-frame-alist) + parameters) (push (cons 'height (cons 'text-pixels (frame-text-height frame))) - default-frame-alist)) - (setq new-frame (make-frame)) + parameters)) + (setq new-frame (make-frame parameters)) (when windows (window-state-put windows (frame-root-window new-frame) 'safe)) (unless (display-graphic-p frame) @@ -995,6 +995,24 @@ frame, unless you add them to the hook in your early-init file.") (defvar x-display-name) +(defun frame--purify-parameters (parameters) + "Return PARAMETERS without internals and ignoring unset parameters. +Use this helper function so that `make-frame' does not override any +parameters. + +In the return value, assign nil to each parameter in +`default-frame-alist', `window-system-default-frame-alist', +`frame-inherited-parameters', which is not in PARAMETERS, and remove all +parameters in `frame-internal-parameters' from PARAMETERS." + (dolist (p (append default-frame-alist + window-system-default-frame-alist + frame-inherited-parameters)) + (unless (assq (car p) parameters) + (push (cons (car p) nil) parameters))) + (seq-remove (lambda (elem) + (memq (car elem) frame-internal-parameters)) + parameters)) + (defun make-frame (&optional parameters) "Return a newly created frame displaying the current buffer. Optional argument PARAMETERS is an alist of frame parameters for @@ -1094,6 +1112,12 @@ current buffer even if it is hidden." (setq params (cons '(minibuffer) (delq (assq 'minibuffer params) params)))) + ;; Let the `frame-creation-function' apparatus assign a new frame id + ;; for a new or cloned frame. For an undeleted frame, send the old + ;; id via a frame parameter. + (when-let* ((id (cdr (assq 'undeleted params)))) + (push (cons 'frame-id id) params)) + ;; Now make the frame. (run-hooks 'before-make-frame-hook) @@ -1125,7 +1149,7 @@ current buffer even if it is hidden." ;; buffers for these windows were set (Bug#79606). (let* ((root (frame-root-window frame)) (buffer (window-buffer root))) - (with-current-buffer buffer + (with-current-buffer buffer (set-window-fringes root left-fringe-width right-fringe-width fringes-outside-margins) (set-window-scroll-bars @@ -1135,7 +1159,7 @@ current buffer even if it is hidden." root left-margin-width right-margin-width))) (let* ((mini (minibuffer-window frame)) (buffer (window-buffer mini))) - (when (eq (window-frame mini) frame) + (when (eq (window-frame mini) frame) (with-current-buffer buffer (set-window-fringes mini left-fringe-width right-fringe-width fringes-outside-margins) @@ -3126,7 +3150,8 @@ Only the 16 most recently deleted frames are saved." ;; to restore a graphical frame. (and (eq (car elem) 'display) (not (display-graphic-p))))) (frame-parameters frame)) - (window-state-get (frame-root-window frame))) + (window-state-get (frame-root-window frame)) + (frame-id frame)) undelete-frame--deleted-frames)) (if (> (length undelete-frame--deleted-frames) 16) (setq undelete-frame--deleted-frames @@ -3149,7 +3174,9 @@ Without a prefix argument, undelete the most recently deleted frame. With a numerical prefix argument ARG between 1 and 16, where 1 is most recently deleted frame, undelete the ARGth deleted frame. -When called from Lisp, returns the new frame." +When called from Lisp, returns the new frame. + +An undeleted frame retains its original frame ID. See `frame-id'." (interactive "P") (if (not undelete-frame-mode) (user-error "Undelete-Frame mode is disabled") @@ -3170,10 +3197,11 @@ When called from Lisp, returns the new frame." (if graphic "graphic" "non-graphic")) (setq undelete-frame--deleted-frames (delq frame-data undelete-frame--deleted-frames)) - (let* ((default-frame-alist - (append `((undeleted . t)) - (nth 1 frame-data))) - (frame (make-frame))) + (let* ((parameters + ;; `undeleted' signals to `make-frame' to reuse its id. + (append `((undeleted . ,(nth 3 frame-data))) + (frame--purify-parameters (nth 1 frame-data)))) + (frame (make-frame parameters))) (window-state-put (nth 2 frame-data) (frame-root-window frame) 'safe) (select-frame-set-input-focus frame) frame)))))))) diff --git a/src/androidfns.c b/src/androidfns.c index 039fcebd2ff..6d9af385ce7 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -858,6 +858,8 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, XSETFRAME (frame, f); + frame_set_id_from_params (f, parms); + f->terminal = dpyinfo->terminal; f->output_method = output_android; diff --git a/src/frame.c b/src/frame.c index 5d38f015130..033215a76ec 100644 --- a/src/frame.c +++ b/src/frame.c @@ -337,6 +337,83 @@ return values. */) : Qnil); } + +/* Frame id. */ + +EMACS_UINT frame_next_id = 1; /* 0 indicates no id (yet) set. */ + +DEFUN ("frame-id", Fframe_id, Sframe_id, 0, 1, 0, + doc: /* Return FRAME's id. +If FRAME is nil, use the selected frame. +Return nil if the id has not been set. */) + (Lisp_Object frame) +{ + if (NILP (frame)) + frame = selected_frame; + struct frame *f = decode_live_frame (frame); + if (f->id == 0) + return Qnil; + else + return make_fixnum (f->id); +} + +/** frame_set_id: Set frame F's id to ID. + + If ID is 0 and F's ID is 0, use frame_next_id and increment it, + otherwise, use ID. + + Signal an error if ID >= frame_next_id. + Signal an error if ID is in use on another live frame. + + Return ID if it was used, 0 otherwise. */ +EMACS_UINT +frame_set_id (struct frame *f, EMACS_UINT id) +{ + if (id >= frame_next_id) + error ("Specified frame ID unassigned"); + + if (id > 0) + { + eassume (CONSP (Vframe_list)); + Lisp_Object frame, tail = Qnil; + FOR_EACH_FRAME (tail, frame) + { + if (id == XFRAME (frame)->id) + error ("Specified frame ID already in use"); + } + } + + if (id == 0) + if (f->id != 0) + return 0; + else + f->id = frame_next_id++; + else + f->id = id; + return f->id; +} + +/** frame_set_id_from_params: Set frame F's id from params, if present. + + Call frame_set_id to using the frame parameter 'frame-id, if present + and a valid positive integer greater than 0, otherwise use 0. + + Return frame_set_id's return value. */ +EMACS_UINT +frame_set_id_from_params (struct frame *f, Lisp_Object params) +{ + EMACS_UINT id = 0; + Lisp_Object param_id = Fcdr (Fassq (Qframe_id, params)); + if (TYPE_RANGED_FIXNUMP (int, param_id)) + { + EMACS_INT id_1 = XFIXNUM (param_id); + if (id_1 > 0) + id = (EMACS_UINT) id_1; + } + return frame_set_id (f, id); +} + + DEFUN ("window-system", Fwindow_system, Swindow_system, 0, 1, 0, doc: /* The name of the window system that FRAME is displaying through. The value is a symbol: @@ -1358,6 +1435,7 @@ make_initial_frame (void) f = make_frame (true); XSETFRAME (frame, f); + frame_set_id (f, 0); Vframe_list = Fcons (frame, Vframe_list); @@ -1742,6 +1820,7 @@ affects all frames on the same terminal device. */) frames don't obscure other frames. */ Lisp_Object parent = Fcdr (Fassq (Qparent_frame, parms)); struct frame *f = make_terminal_frame (t, parent, parms); + frame_set_id_from_params (f, parms); if (!noninteractive) init_frame_faces (f); @@ -7195,6 +7274,7 @@ syms_of_frame (void) DEFSYM (Qfont_parameter, "font-parameter"); DEFSYM (Qforce, "force"); DEFSYM (Qinhibit, "inhibit"); + DEFSYM (Qframe_id, "frame-id"); DEFSYM (Qcloned_from, "cloned-from"); DEFSYM (Qundeleted, "undeleted"); @@ -7581,6 +7661,7 @@ allow `make-frame' to show the current buffer even if its hidden. */); #else frame_internal_parameters = list3 (Qname, Qparent_id, Qwindow_id); #endif + frame_internal_parameters = Fcons (Qframe_id, frame_internal_parameters); frame_internal_parameters = Fcons (Qcloned_from, frame_internal_parameters); frame_internal_parameters = Fcons (Qundeleted, frame_internal_parameters); @@ -7607,6 +7688,7 @@ The default is \\+`inhibit' in NS builds and nil everywhere else. */); alter_fullscreen_frames = Qnil; #endif + defsubr (&Sframe_id); defsubr (&Sframep); defsubr (&Sframe_live_p); defsubr (&Swindow_system); diff --git a/src/frame.h b/src/frame.h index 9feadeef0a5..c369a848b7c 100644 --- a/src/frame.h +++ b/src/frame.h @@ -292,6 +292,9 @@ struct frame struct image_cache *image_cache; #endif /* HAVE_WINDOW_SYSTEM */ + /* Unique frame id. */ + EMACS_UINT id; + /* Tab-bar item index of the item on which a mouse button was pressed. */ int last_tab_bar_item; @@ -1415,6 +1418,10 @@ FRAME_PARENT_FRAME (struct frame *f) #define AUTO_FRAME_ARG(name, parameter, value) \ AUTO_LIST1 (name, AUTO_CONS_EXPR (parameter, value)) +extern EMACS_UINT frame_next_id; +extern EMACS_UINT frame_set_id (struct frame *f, EMACS_UINT id); +extern EMACS_UINT frame_set_id_from_params (struct frame *f, Lisp_Object params); + /* False means there are no visible garbaged frames. */ extern bool frame_garbaged; diff --git a/src/haikufns.c b/src/haikufns.c index 21507a43a26..e24dfd2193e 100644 --- a/src/haikufns.c +++ b/src/haikufns.c @@ -750,6 +750,8 @@ haiku_create_frame (Lisp_Object parms) XSETFRAME (frame, f); + frame_set_id_from_params (f, parms); + f->terminal = dpyinfo->terminal; f->output_method = output_haiku; diff --git a/src/nsfns.m b/src/nsfns.m index 4bd488478a8..cf685630ab7 100644 --- a/src/nsfns.m +++ b/src/nsfns.m @@ -1262,6 +1262,8 @@ Turn the input menu (an NSMenu) into a lisp list for tracking on lisp side. XSETFRAME (frame, f); + frame_set_id_from_params (f, parms); + f->terminal = dpyinfo->terminal; f->output_method = output_ns; diff --git a/src/pgtkfns.c b/src/pgtkfns.c index b7c2d850550..c336ce36d58 100644 --- a/src/pgtkfns.c +++ b/src/pgtkfns.c @@ -1283,6 +1283,8 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, 1, 1, 0, XSETFRAME (frame, f); + frame_set_id_from_params (f, parms); + f->terminal = dpyinfo->terminal; f->output_method = output_pgtk; diff --git a/src/w32fns.c b/src/w32fns.c index d8093e0bd93..b75bce8d1a2 100644 --- a/src/w32fns.c +++ b/src/w32fns.c @@ -6315,6 +6315,8 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, XSETFRAME (frame, f); + frame_set_id_from_params (f, parameters); + parent_frame = gui_display_get_arg (dpyinfo, parameters, Qparent_frame, NULL, NULL, RES_TYPE_SYMBOL); diff --git a/src/xfns.c b/src/xfns.c index 4ab2e40fc3c..70a4b6d5509 100644 --- a/src/xfns.c +++ b/src/xfns.c @@ -5020,6 +5020,8 @@ This function is an internal primitive--use `make-frame' instead. */) XSETFRAME (frame, f); + frame_set_id_from_params (f, parms); + f->terminal = dpyinfo->terminal; f->output_method = output_x_window;