Add predicate for initial_terminal

This introduces the predicate frame-initial-p, which uses
struct frame.output_method or struct terminal.type to detect
initial_terminal without relying on its name (bug#80629).
For some prior discussion, see:
https://lists.gnu.org/r/emacs-devel/2019-12/msg00480.html
https://lists.gnu.org/r/emacs-devel/2020-01/msg00120.html

* doc/lispref/frames.texi (Frames): Document frame-initial-p.
(Finding All Frames): Fix grammar.
* etc/NEWS (Lisp Changes in Emacs 31.1): Announce frame-initial-p.
* lisp/desktop.el (desktop--check-dont-save):
* lisp/emacs-lisp/debug.el (debug):
* lisp/frameset.el (frameset-restore):
* lisp/menu-bar.el (menu-bar-update-buffers):
* lisp/xt-mouse.el (turn-on-xterm-mouse-tracking-on-terminal):
Use frame-initial-p instead of checking the "initial_terminal" name.
* lisp/emacs-lisp/byte-opt.el: Mark frame-initial-p as error-free.

* src/pgtkterm.c (pgtk_focus_changed): Use IS_DAEMON in place of
Fdaemonp, thus also accepting a named daemon session.
* src/terminal.c (decode_tty_terminal): Clarify commentary.
(Fframe_initial_p): New function.
(syms_of_terminal): Expose it.
(init_initial_terminal): Update commentary now that
menu-bar-update-buffers uses frame-initial-p (bug#53740).

* test/lisp/xt-mouse-tests.el (with-xterm-mouse-mode): Simulate the
lack of an initial terminal by overriding frame-initial-p now
that turn-on-xterm-mouse-tracking-on-terminal uses it.
* test/src/terminal-tests.el: New file.
This commit is contained in:
Basil L. Contovounesios
2026-03-18 12:42:28 +01:00
parent dfeaf7fc00
commit d780007283
20 changed files with 131 additions and 28 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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))

View File

@@ -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)))

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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)))

View File

@@ -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))))

View File

@@ -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)

View File

@@ -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)))))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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;

View File

@@ -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);

View File

@@ -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))))

View File

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