diff --git a/doc/lispref/frames.texi b/doc/lispref/frames.texi index d57d643e922..c50619a2de0 100644 --- a/doc/lispref/frames.texi +++ b/doc/lispref/frames.texi @@ -89,6 +89,20 @@ displayed on that terminal; the list of possible values is the same as for @code{framep} above. @end defun +@defun frame-initial-p &optional frame +This predicate returns non-@code{nil} if @var{frame} is or holds the +initial text frame that is used internally during daemon mode +(@pxref{Initial Options, daemon,, emacs, The GNU Emacs Manual}), batch +mode (@pxref{Batch Mode}), and the early stages of startup +(@pxref{Startup Summary}). Interactive and graphical programs, for +instance, can use this predicate to avoid operating on the initial +frame, which is never displayed. + +If @var{frame} is a terminal, this function returns non-@code{nil} if +@var{frame} holds the initial frame. If @var{frame} is omitted or +@code{nil}, it defaults to the selected one. +@end defun + @cindex top-level frame On a graphical terminal we distinguish two types of frames: A normal @dfn{top-level frame} is a frame whose window-system window is a child @@ -3029,7 +3043,7 @@ direction. See also @code{next-window} and @code{previous-window}, in @ref{Cyclic Window Ordering}. - Some Lisp programs need to find one or more frames that satisfy a + Some Lisp programs need to find one or more frames that satisfy given criteria. The function @code{filtered-frame-list} is provided for this purpose. diff --git a/etc/NEWS b/etc/NEWS index 9bdd2afedc9..72788da5bd5 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -4101,6 +4101,14 @@ to display its char argument on a given frame. This new function, unlike 'char-displayable-p', does not check whether the character can be encoded by the underlying terminal. ++++ +** New function 'frame-initial-p'. +This predicate returns non-nil if a given frame or terminal is or holds, +respectively, the initial text frame that is used internally during +daemon mode, batch mode, and the early stages of startup. Interactive +and graphical programs, for instance, can use this predicate to avoid +operating on the initial frame, which is never displayed. + +++ ** New macros 'static-when' and 'static-unless'. Like 'static-if', these macros evaluate their condition at diff --git a/lisp/desktop.el b/lisp/desktop.el index f478cf2307b..0cdd554e295 100644 --- a/lisp/desktop.el +++ b/lisp/desktop.el @@ -775,6 +775,7 @@ if different)." ;; Don't delete daemon's initial frame, or ;; we'll never be able to close the last ;; client's frame (Bug#26912). + ;; Use `frame-initial-p'? (and (daemonp) (eq frame terminal-frame)) (frame-parameter frame 'desktop-dont-clear)) (delete-frame frame)) @@ -1067,9 +1068,8 @@ DIRNAME must be the directory in which the desktop file will be saved." (and (not (frame-parameter frame 'desktop-dont-save)) ;; Don't save daemon initial frames, since we cannot (and don't ;; need to) restore them. - (not (and (daemonp) - (equal (terminal-name (frame-terminal frame)) - "initial_terminal"))))) + (not (and (daemonp) ;; FIXME: Remove `daemonp'? + (frame-initial-p frame))))) (defconst desktop--app-id `(desktop . ,desktop-file-version)) @@ -1260,7 +1260,7 @@ This function also sets `desktop-dirname' to nil." "True if calling `desktop-restore-frameset' will actually restore it." (and desktop-restore-frames desktop-saved-frameset ;; Don't restore frames when the selected frame is the daemon's - ;; initial frame. + ;; initial frame. Use `frame-initial-p'? (not (and (daemonp) (eq (selected-frame) terminal-frame))) t)) diff --git a/lisp/display-fill-column-indicator.el b/lisp/display-fill-column-indicator.el index 349a470ab41..b661f20e22a 100644 --- a/lisp/display-fill-column-indicator.el +++ b/lisp/display-fill-column-indicator.el @@ -102,6 +102,7 @@ See Info node `Displaying Boundaries' for details." (defun display-fill-column-indicator--turn-on () "Turn on `display-fill-column-indicator-mode'." (unless (or (minibufferp) + ;; Use `frame-initial-p'? (and (daemonp) (eq (selected-frame) terminal-frame))) (display-fill-column-indicator-mode))) diff --git a/lisp/emacs-lisp/byte-opt.el b/lisp/emacs-lisp/byte-opt.el index ce2d8ac47c4..7ed71346451 100644 --- a/lisp/emacs-lisp/byte-opt.el +++ b/lisp/emacs-lisp/byte-opt.el @@ -1901,6 +1901,8 @@ See Info node `(elisp) Integer Basics'." sqlite-available-p sqlitep ;; syntax.c standard-syntax-table syntax-table syntax-table-p + ;; terminal.c + frame-initial-p ;; thread.c current-thread ;; timefns.c diff --git a/lisp/emacs-lisp/debug.el b/lisp/emacs-lisp/debug.el index 3019ada1bbd..ec2aa0ad728 100644 --- a/lisp/emacs-lisp/debug.el +++ b/lisp/emacs-lisp/debug.el @@ -195,8 +195,7 @@ the debugger will not be entered." ;; backtrace to stdout. This happens for example while ;; handling an error in code from early-init.el with ;; --debug-init. - (and (eq t (framep (selected-frame))) - (equal "initial_terminal" (terminal-name))))) + (frame-initial-p))) ;; Don't let `inhibit-message' get in our way (especially important if ;; `non-interactive-frame' evaluated to a non-nil value. (inhibit-message nil) diff --git a/lisp/emacs-lisp/warnings.el b/lisp/emacs-lisp/warnings.el index ddf3b594e12..7db316acda7 100644 --- a/lisp/emacs-lisp/warnings.el +++ b/lisp/emacs-lisp/warnings.el @@ -372,6 +372,7 @@ entirely by setting `warning-suppress-types' or (if (bolp) (forward-char -1)) (message "%s" (buffer-substring start (point)))))) + ;; Use `frame-initial-p'? ((and (daemonp) (eq (selected-frame) terminal-frame)) ;; Display daemon startup warnings on the first client frame. (letrec ((afterfun diff --git a/lisp/frame.el b/lisp/frame.el index da48e695297..85b58cee070 100644 --- a/lisp/frame.el +++ b/lisp/frame.el @@ -493,6 +493,7 @@ there (in decreasing order of priority)." (setq parms (append initial-frame-alist window-system-frame-alist default-frame-alist parms nil)) ;; Don't enable tab-bar in daemon's initial frame. + ;; Use `frame-initial-p'? (when (and (daemonp) (eq (selected-frame) terminal-frame)) (setq parms (delq (assq 'tab-bar-lines parms) parms))) parms)) diff --git a/lisp/frameset.el b/lisp/frameset.el index e11a1da7e9b..0dde10869fd 100644 --- a/lisp/frameset.el +++ b/lisp/frameset.el @@ -1370,12 +1370,10 @@ All keyword parameters default to nil." ;; frame, as that would only trigger ;; warnings. (not - (and (daemonp) - (equal (terminal-name (frame-terminal - frame)) - "initial_terminal")))) - (delete-frame frame))) - cleanup-frames))) + (and (daemonp) ;; FIXME: Remove `daemonp'? + (frame-initial-p frame)))) + (delete-frame frame))) + cleanup-frames))) (maphash (lambda (frame _action) (push frame map)) frameset--action-map) (dolist (frame (sort map ;; Minibufferless frames must go first to avoid diff --git a/lisp/menu-bar.el b/lisp/menu-bar.el index b1d7bd83983..f96cd43eca6 100644 --- a/lisp/menu-bar.el +++ b/lisp/menu-bar.el @@ -2496,8 +2496,7 @@ It must accept a buffer as its only required argument.") ;; Ignore the initial frame if present. It can happen if ;; Emacs was started as a daemon. (bug#53740) (dolist (frame (frame-list)) - (unless (equal (terminal-name (frame-terminal frame)) - "initial_terminal") + (unless (frame-initial-p frame) (push frame frames))) ;; Make the menu of buffers proper. (setq buffers-menu diff --git a/lisp/obsolete/linum.el b/lisp/obsolete/linum.el index 5a0a67ebff0..9b0efaf223a 100644 --- a/lisp/obsolete/linum.el +++ b/lisp/obsolete/linum.el @@ -129,6 +129,7 @@ Linum mode is a buffer-local minor mode." ;; Note that nowadays, this actually doesn't show line ;; numbers in client frames at all, because we visit the ;; file before creating the client frame. See bug#35726. + ;; Use `frame-initial-p'? (and (daemonp) (eq (selected-frame) terminal-frame))) (linum-mode 1))) diff --git a/lisp/progmodes/flymake.el b/lisp/progmodes/flymake.el index 4e828eba8a0..f62f9f5ce3c 100644 --- a/lisp/progmodes/flymake.el +++ b/lisp/progmodes/flymake.el @@ -1330,6 +1330,8 @@ Interactively, with a prefix arg, FORCE is t." (buffer (current-buffer))) (cl-labels ((visible-buffer-window () + ;; This can use `frame-initial-p' once + ;; we can assume Emacs 31 or later. (and (or (not (daemonp)) (not (eq (selected-frame) terminal-frame))) (get-buffer-window (current-buffer)))) diff --git a/lisp/server.el b/lisp/server.el index fcfc6c01972..f5dea9c590f 100644 --- a/lisp/server.el +++ b/lisp/server.el @@ -706,6 +706,7 @@ the `server-process' variable." ;; when we can't get user input, which may happen when ;; doing emacsclient --eval "(kill-emacs)" in daemon mode. (cond + ;; Use `frame-initial-p'? ((and (daemonp) (null (cdr (frame-list))) (eq (selected-frame) terminal-frame)) @@ -1429,6 +1430,7 @@ The following commands are accepted by the client: (or (eq use-current-frame 'always) ;; We can't use the Emacs daemon's ;; terminal frame. + ;; Use `frame-initial-p'? (not (and (daemonp) (null (cdr (frame-list))) (eq (selected-frame) @@ -1453,6 +1455,7 @@ The following commands are accepted by the client: ;; If there won't be a current frame to use, fall ;; back to trying to create a new one. ((and use-current-frame + ;; Use `frame-initial-p'? (daemonp) (null (cdr (frame-list))) (eq (selected-frame) terminal-frame) diff --git a/lisp/tab-bar.el b/lisp/tab-bar.el index ad749557987..3399e5ef93e 100644 --- a/lisp/tab-bar.el +++ b/lisp/tab-bar.el @@ -292,6 +292,7 @@ a list of frames to update." (and (eq auto-resize-tab-bars 'grow-only) (> (frame-parameter frame 'tab-bar-lines) 1)) ;; Don't enable tab-bar in daemon's initial frame. + ;; Use `frame-initial-p'? (and (daemonp) (eq frame terminal-frame))) (set-frame-parameter frame 'tab-bar-lines (tab-bar--tab-bar-lines-for-frame frame))))) diff --git a/lisp/vc/vc-hooks.el b/lisp/vc/vc-hooks.el index 042733f4c61..2dcae7362b7 100644 --- a/lisp/vc/vc-hooks.el +++ b/lisp/vc/vc-hooks.el @@ -977,6 +977,7 @@ In the latter case, VC mode is deactivated for this buffer." noninteractive ;; Copied from server-start. Seems like there should ;; be a better way to ask "can we get user input?"... + ;; Use `frame-initial-p'? (and (daemonp) (null (cdr (frame-list))) (eq (selected-frame) terminal-frame)) diff --git a/lisp/xt-mouse.el b/lisp/xt-mouse.el index 67c475d563a..b93d914380f 100644 --- a/lisp/xt-mouse.el +++ b/lisp/xt-mouse.el @@ -509,16 +509,14 @@ enable, ?l to disable)." "Enable xterm mouse tracking on TERMINAL." (when (and xterm-mouse-mode (eq t (terminal-live-p terminal)) ;; Avoid the initial terminal which is not a termcap device. - ;; FIXME: is there more elegant way to detect the initial - ;; terminal? - (not (string= (terminal-name terminal) "initial_terminal"))) + (not (frame-initial-p terminal))) (unless (terminal-parameter terminal 'xterm-mouse-mode) ;; Simulate selecting a terminal by selecting one of its frames ;; so that we can set the terminal-local `input-decode-map'. ;; Use the tty-top-frame to avoid accidentally making an invisible ;; child frame visible by selecting it (bug#79960). - ;; The test for match mode is here because xt-mouse-tests run in - ;; match mode, and there is no top-frame in that case. + ;; The test for batch mode is here because xt-mouse-tests run in + ;; batch mode, and there is no top-frame in that case. (with-selected-frame (if noninteractive (car (frame-list)) (tty-top-frame terminal)) diff --git a/src/pgtkterm.c b/src/pgtkterm.c index bf482590bc5..6dc45604f01 100644 --- a/src/pgtkterm.c +++ b/src/pgtkterm.c @@ -5693,10 +5693,11 @@ pgtk_focus_changed (gboolean is_enter, int state, /* Don't stop displaying the initial startup message for a switch-frame event we don't need. */ - /* When run as a daemon, Vterminal_frame is always NIL. */ + /* When run as a daemon, Vterminal_frame is always nil. + FIXME: Isn't it actually the other way around? */ bufp->ie.arg = (((NILP (Vterminal_frame) || !FRAME_PGTK_P (XFRAME (Vterminal_frame)) - || EQ (Fdaemonp (), Qt)) + || IS_DAEMON) && CONSP (Vframe_list) && !NILP (XCDR (Vframe_list))) ? Qt : Qnil); bufp->ie.kind = FOCUS_IN_EVENT; diff --git a/src/terminal.c b/src/terminal.c index 44095acc2ab..f3a90740d8e 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -244,8 +244,8 @@ decode_live_terminal (Lisp_Object terminal) return t; } -/* Like decode_terminal, but ensure that the resulting terminal object refers - to a text-based terminal device. */ +/* Like decode_live_terminal, but ensure that the resulting terminal + object refers to a text-based terminal device. */ struct terminal * decode_tty_terminal (Lisp_Object terminal) @@ -479,6 +479,25 @@ return values. */) } } +DEFUN ("frame-initial-p", Fframe_initial_p, Sframe_initial_p, 0, 1, 0, + doc: /* Return non-nil if FRAME is the initial frame. +That is, the initial text frame used internally during daemon mode, +batch mode, and the early stages of startup. +If FRAME is a terminal object, return non-nil if it holds +the initial frame. FRAME defaults to the selected frame. */) + (Lisp_Object frame) +{ + if (NILP (frame)) + frame = selected_frame; + if (FRAMEP (frame)) + { + struct frame *f = XFRAME (frame); + return FRAME_LIVE_P (f) && FRAME_INITIAL_P (f) ? Qt : Qnil; + } + struct terminal *t = decode_terminal (frame); + return t && t->type == output_initial ? Qt : Qnil; +} + DEFUN ("terminal-list", Fterminal_list, Sterminal_list, 0, 0, 0, doc: /* Return a list of all terminal devices. */) (void) @@ -647,8 +666,6 @@ init_initial_terminal (void) emacs_abort (); initial_terminal = create_terminal (output_initial, NULL); - /* Note: menu-bar.el:menu-bar-update-buffers knows about this - special name of the initial terminal. */ initial_terminal->name = xstrdup ("initial_terminal"); initial_terminal->kboard = initial_kboard; initial_terminal->delete_terminal_hook = &delete_initial_terminal; @@ -688,12 +705,14 @@ or some time later. */); Vdelete_terminal_functions = Qnil; DEFSYM (Qterminal_live_p, "terminal-live-p"); + DEFSYM (Qframe_initial_p, "frame-initial-p"); DEFSYM (Qdelete_terminal_functions, "delete-terminal-functions"); DEFSYM (Qrun_hook_with_args, "run-hook-with-args"); defsubr (&Sdelete_terminal); defsubr (&Sframe_terminal); defsubr (&Sterminal_live_p); + defsubr (&Sframe_initial_p); defsubr (&Sterminal_list); defsubr (&Sterminal_name); defsubr (&Sterminal_parameters); diff --git a/test/lisp/xt-mouse-tests.el b/test/lisp/xt-mouse-tests.el index 26fe5002b68..b065fda5eed 100644 --- a/test/lisp/xt-mouse-tests.el +++ b/test/lisp/xt-mouse-tests.el @@ -50,8 +50,7 @@ ;; `xterm-mouse-mode' doesn't work in the initial ;; terminal. Since we can't create a second ;; terminal in batch mode, fake it temporarily. - (cl-letf (((symbol-function 'terminal-name) - (lambda (&optional _terminal) "fake-terminal"))) + (cl-letf (((symbol-function 'frame-initial-p) #'ignore)) (xterm-mouse-mode 1)) ,@body) (xterm-mouse-mode 0)))) diff --git a/test/src/terminal-tests.el b/test/src/terminal-tests.el new file mode 100644 index 00000000000..85c4fa04efc --- /dev/null +++ b/test/src/terminal-tests.el @@ -0,0 +1,55 @@ +;;; terminal-tests.el --- tests for terminal.c -*- lexical-binding: t -*- + +;; Copyright (C) 2026 Free Software Foundation, Inc. + +;; 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 . + +;;; Code: + +(require 'ert) + +(ert-deftest frame-initial-p () + "Test `frame-initial-p' behavior." + (should-not (frame-initial-p t)) + (should-not (frame-initial-p (current-buffer))) + (should-not (frame-initial-p (selected-window))) + ;; "Initial frame" implies "initial terminal", and + ;; no other terminal can have the initial frame. + (should-not (xor (equal (terminal-name) "initial_terminal") + (frame-initial-p))) + ;; Initial frame implies its terminal is a termcap-like + ;; text-mode terminal. + (should (or (not (frame-initial-p)) + (eq (terminal-live-p nil) t))) + ;; It similarly implies a termcap-like text-mode frame. + (should (or (not (frame-initial-p)) + (eq (frame-live-p (selected-frame)) t))) + (dolist (ft (append '(nil) (frame-list) (terminal-list))) + (ert-info ((prin1-to-string ft) :prefix "Argument: ") + (should-not (xor (equal (terminal-name ft) "initial_terminal") + (frame-initial-p ft))) + (should (or (not (frame-initial-p ft)) + (eq (terminal-live-p ft) t))))) + (cond (noninteractive + ;; Batch mode should have an initial frame. + (should (any #'frame-initial-p (frame-list))) + (should (any #'frame-initial-p (terminal-list)))) + ((not (daemonp)) + ;; Non-daemon interactive mode should have none. + (should-not (any #'frame-initial-p (frame-list))) + (should-not (any #'frame-initial-p (terminal-list)))))) + +;;; terminal-tests.el ends here