@finalout
@titlepage
@title Transient User and Developer Manual
-@subtitle for version 0.8.6
+@subtitle for version 0.9.1
@author Jonas Bernoulli
@page
@vskip 0pt plus 1filll
available at @uref{https://github.com/positron-solutions/transient-showcase}.
@noindent
-This manual is for Transient version 0.8.6.
+This manual is for Transient version 0.9.1.
@insertcopying
@end ifnottex
pressing the key that is bound to that command. The main difference
to other commands is that a transient prefix command activates a
transient keymap, which temporarily binds the transient's infix and
-suffix commands, and that those bindings are displayed in a transient
-menu, displayed in a popup buffer. Bindings from other keymaps may,
-or may not, be disabled while the transient state is in effect.
+suffix commands, and that those bindings are shown in menu buffer,
+which is displayed in a new window, until the menu is exited.
+Bindings from other keymaps may, or may not, be disabled while the
+transient state is in effect.
There are two kinds of commands that are available after invoking a
transient prefix command; infix and suffix commands. Infix commands
-set some value (which is then shown in the popup buffer), without
+set some value (which is then shown in the menu buffer), without
leaving the transient. Suffix commands, on the other hand, usually
quit the transient and they may use the values set by the infix
commands, i.e., the infix @strong{arguments}.
A transient prefix command can be bound as a suffix of another
transient. Invoking such a suffix replaces the current transient
state with a new transient state, i.e., the available bindings change
-and the information displayed in the popup buffer is updated
+and the information displayed in the menu buffer is updated
accordingly. Pressing @kbd{C-g} while a nested transient is active only
quits the innermost transient, causing a return to the previous
transient.
@cindex common suffix commands
A few shared suffix commands are available in all transients. These
-suffix commands are not shown in the popup buffer by default.
+suffix commands are not shown permanently in every menu by default.
+Most of these commands share a common prefix key and pressing that key
+causes the common commands to be temporarily shown in the active menu.
-This includes the aborting commands mentioned in the previous section,
-as well as some other commands that are all bound to @kbd{C-x @var{KEY}}. After
-@kbd{C-x} is pressed, a section featuring all these common commands is
-temporarily shown in the popup buffer. After invoking one of them,
-the section disappears again. Note, however, that one of these
-commands is described as ``Show common permanently''; invoke that if you
-want the common commands to always be shown for all transients.
+@defopt transient-show-common-commands
+This option controls whether shared suffix commands are permanently
+shown alongside the menu-specific infix and suffix commands. By
+default, the shared commands are not permanently shown to avoid
+wasting precious space and overwhelming the user with too many
+choices.
+
+If you prefer to always see these commands, then set this option to
+a non-@code{nil} value. Alternatively the value can be toggled for the
+current Emacs session only, using @code{transient-toggle-common}, described
+below.
+@end defopt
+
+@defopt transient-common-command-prefix
+This option specifies the prefix key used in all transient menus
+to invoke most of the shared commands, which are available in all
+transient menus. By default these bindings are only shown after
+pressing that prefix key and before following that up with a valid
+key binding (but see the previous option).
+
+For historic reasons @kbd{C-x} is used by default, but users are
+encouraged to pick another key, preferably one that is not commonly
+used in Emacs but is still convenient to them.
+
+Usually, while a transient menu is active, the user cannot invoke
+commands that are not bound in the menu itself. For those menus it
+does not matter, if @kbd{C-x} or another commonly used prefix key is used
+for common menu commands. However, certain other, newer menus do
+not suppress key bindings established outside the menu itself, and
+in those cases a binding for a common menu command could shadow an
+external binding. For example, @kbd{C-x C-s} could not be used to invoke
+@code{save-buffer}, if that binding is shadowed by the menu binding for
+@code{transient-save}.
+
+Which key is most suitable depends on the user's preferences, but
+good choices may include function keys and @kbd{C-z} (for many keyboard
+layouts @kbd{z} is right next to @kbd{x}, and invoking @code{suspend-frame}, while a
+transient menu is active, would not be a good idea anyway).
+@end defopt
@table @asis
@item @kbd{C-x t} (@code{transient-toggle-common})
@kindex C-x t
@findex transient-toggle-common
-This command toggles whether the generic commands that are common to
-all transients are always displayed or only after typing the
-incomplete prefix key sequence @kbd{C-x}. This only affects the current
+This command toggles whether the generic commands, that are common
+to all transients, are permanently displayed or only after typing
+the incomplete prefix key sequence@kbd{}. This only affects the current
Emacs session.
@end table
-@defopt transient-show-common-commands
-This option controls whether shared suffix commands are shown
-alongside the transient-specific infix and suffix commands. By
-default, the shared commands are not shown to avoid overwhelming
-the user with too many options.
-
-While a transient is active, pressing @kbd{C-x} always shows the common
-commands. The value of this option can be changed for the current
-Emacs session by typing @kbd{C-x t} while a transient is active.
-@end defopt
-
The other common commands are described in either the previous or in
one of the following sections.
history. That value won't be used when the transient is next invoked,
but it is easily accessible (@pxref{Using History}).
+Option @code{transient-common-command-prefix} controls the prefix key used
+in the following bindings. For simplicity's sake the default, @kbd{C-x},
+is shown below.
+
@table @asis
@item @kbd{C-x s} (@code{transient-set})
@kindex C-x s
the same way one can cycle through the history of commands that read
user-input in the minibuffer.
+Option @code{transient-common-command-prefix} controls the prefix key used
+in the following bindings. For simplicity's sake the default, @kbd{C-x},
+is shown below.
+
@table @asis
@item @kbd{C-M-p} (@code{transient-history-prev})
@itemx @kbd{C-x p}
displayed at any level.
The levels of individual transients and/or their individual suffixes
-can be changed interactively, by invoking the transient and then
-pressing @kbd{C-x l} to enter the ``edit'' mode, see below.
+can be changed interactively, by invoking the menu and entering its
+``edit'' mode using the command @code{transient-set-level}, as described below.
The default level for both transients and their suffixes is 4. The
@code{transient-default-level} option only controls the default for
transients and their suffixes between Emacs sessions.
@end defopt
+Option @code{transient-common-command-prefix} controls the prefix key used
+in the following bindings. For simplicity's sake the default, @kbd{C-x},
+is shown below.
+
@table @asis
@item @kbd{C-x l} (@code{transient-set-level})
@kindex C-x l
@code{scroll-down-command} in other buffers.
@deffn Command transient-scroll-up arg
-This command scrolls text of transient popup window upward @var{ARG}
+This command scrolls text of transient's menu window upward @var{ARG}
lines. If @var{ARG} is @code{nil}, then it scrolls near full screen. This
is a wrapper around @code{scroll-up-command} (which see).
@end deffn
@deffn Command transient-scroll-down arg
-This command scrolls text of transient popup window down @var{ARG}
+This command scrolls text of transient's menu window down @var{ARG}
lines. If @var{ARG} is @code{nil}, then it scrolls near full screen. This
is a wrapper around @code{scroll-down-command} (which see).
@end deffn
@anchor{Essential Options}
@subheading Essential Options
-Also see @ref{Common Suffix Commands}.
+Two more essential options are documented in @ref{Common Suffix Commands}.
@defopt transient-show-popup
-This option controls whether the current transient's infix and
-suffix commands are shown in the popup buffer.
+This option controls whether and when transient's menu buffer is
+shown.
@itemize
@item
-If @code{t} (the default) then the popup buffer is shown as soon as a
+If @code{t} (the default), then the buffer is shown as soon as a
transient prefix command is invoked.
@item
-If @code{nil}, then the popup buffer is not shown unless the user
-explicitly requests it, by pressing an incomplete prefix key
-sequence.
+If @code{nil}, then the buffer is not shown unless the user explicitly
+requests it, by pressing an incomplete prefix key sequence.
@item
If a number, then the a brief one-line summary is shown instead of
-the popup buffer. If zero or negative, then not even that summary
+the menu buffer. If zero or negative, then not even that summary
is shown; only the pressed key itself is shown.
-The popup is shown when the user explicitly requests it by
+The buffer is shown once the user explicitly requests it by
pressing an incomplete prefix key sequence. Unless this is zero,
-the popup is shown after that many seconds of inactivity (using
-the absolute value).
+the menu is shown after that many seconds of inactivity (using the
+absolute value).
@end itemize
@end defopt
-@defopt transient-show-common-commands
-This option controls whether shared suffix commands are shown
-alongside the transient-specific infix and suffix commands. By
-default, the shared commands are not shown to avoid overwhelming
-the user with too many options.
-
-While a transient is active, pressing @kbd{C-x} always shows the common
-commands. The value of this option can be changed for the current
-Emacs session by typing @kbd{C-x t} while a transient is active.
-@end defopt
-
@defopt transient-show-during-minibuffer-read
This option controls whether the transient menu continues to be
displayed while the minibuffer is used to read user input.
@end defopt
@defopt transient-enable-popup-navigation
-This option controls whether navigation commands are enabled in the
-transient popup buffer. If the value is @code{verbose} (the default),
+This option controls whether navigation commands are enabled in
+transient's menu buffer. If the value is @code{verbose} (the default),
brief documentation about the command under point is additionally
show in the echo area.
-While a transient is active the transient popup buffer is not the
-current buffer, making it necessary to use dedicated commands to act
-on that buffer itself. If this option is non-@code{nil}, then the
-following features are available:
+While a transient is active the menu buffer is not the current
+buffer, making it necessary to use dedicated commands to act on that
+buffer itself. If this option is non-@code{nil}, then the following
+features are available:
@itemize
@item
@item
@kbd{mouse-1} invokes the clicked on suffix.
@item
-@kbd{C-s} and @kbd{C-r} start isearch in the popup buffer.
+@kbd{C-s} and @kbd{C-r} start isearch in the menu buffer.
@end itemize
By default @kbd{M-@key{RET}} is bound to @code{transient-push-button}, instead of
@end defopt
@defopt transient-display-buffer-action
-This option specifies the action used to display the transient popup
-buffer. The transient popup buffer is displayed in a window using
+This option specifies the action used to display the transient's
+menu buffer. The menu buffer is displayed in a window using
@code{(display-buffer @var{BUFFER} transient-display-buffer-action)}.
The value of this option has the form @code{(@var{FUNCTION} . @var{ALIST})},
@subheading Auxiliary Options
@defopt transient-mode-line-format
-This option controls whether the transient popup buffer has a
+This option controls whether transient's menu buffer has a
mode-line, separator line, or neither.
If @code{nil}, then the buffer has no mode-line. If the buffer is not
@defopt transient-align-variable-pitch
This option controls whether columns are aligned pixel-wise in the
-popup buffer.
+menu buffer.
If this is non-@code{nil}, then columns are aligned pixel-wise to support
variable-pitch fonts. Keys are not aligned, so you should use a
@defopt transient-force-fixed-pitch
This option controls whether to force the use of a monospaced font
-in popup buffer. Even if you use a proportional font for the
-@code{default} face, you might still want to use a monospaced font in
-transient's popup buffer. Setting this option to @code{t} causes @code{default}
-to be remapped to @code{fixed-pitch} in that buffer.
+in menu buffer. Even if you use a proportional font for the @code{default}
+face, you might still want to use a monospaced font in the menu
+buffer. Setting this option to @code{t} causes @code{default} to be remapped to
+@code{fixed-pitch} in that buffer.
@end defopt
@anchor{Developer Options}
enabled at a time.
@end defopt
+@defopt transient-error-on-insert-failure
+This option controls whether to signal an error when
+@code{transient-insert-suffix} or @code{transient-append-suffix} failed to insert
+a suffix into an existing prefix. By default a warning is shown
+instead.
+@end defopt
+
@defopt transient-highlight-higher-levels
This option controls whether suffixes that would not be available by
default are highlighted.
@subheading Hook Variables
@defvar transient-exit-hook
-This hook is run after a transient is exited.
+This hook is run after a transient menu is exited, even if another
+transient menu becomes active at the same time.
+@end defvar
+
+@defvar transient-post-exit-hook
+This hook is run after a transient menu is exited, provided no other
+transient menu becomes active at the same time.
@end defvar
@defvar transient-setup-buffer-hook
@end lisp
This inserts a new infix argument to toggle the @code{--reverse} argument
-after the infix argument that toggles @code{-3} in @code{magit-patch-apply}.
+after the infix argument that is bound to @code{-3} in @code{magit-patch-apply}.
The following functions share a few arguments:
@item
@var{PREFIX} is a transient prefix command, a symbol.
+PREFIX may also by a symbol identifying a separately defined group,
+which can be included in multiple prefixes. See TODO@.
+
@item
@var{SUFFIX} is a transient infix or suffix specification in the same form
as expected by @code{transient-define-prefix}. Note that an infix is a
@code{transient-define-prefix}. @xref{Group Specifications}.
@item
-@var{LOC} is a command, a key vector, a key description (a string as
-returned by @code{key-description}), or a list specifying coordinates (the
-last element may also be a command or key). For example @code{(1 0 -1)}
+@var{LOC} is a key description (a string as returned by @code{key-description}
+and understood by @code{kbd}), a command, a symbol identifying an included
+group, or a vector specifying coordinates. For example, @code{[1 0 -1]}
identifies the last suffix (@code{-1}) of the first subgroup (@code{0}) of the
second group (@code{1}).
-If @var{LOC} is a list of coordinates, then it can be used to identify a
-group, not just an individual suffix command.
+If @var{LOC} is a vector, then it can be used to identify a group, not
+just an individual suffix command. The last element in a vector may
+also be a symbol or key, in which case the preceding elements must
+match a group and the last element is looked up within that group.
The function @code{transient-get-suffix} can be useful to determine whether
-a certain coordination list identifies the suffix or group that you
+a certain coordinate vector identifies the suffix or group that you
expect it to identify. In hairy cases it may be necessary to look
-at the definition of the transient prefix command.
+at the internal layout representation, which you can access using
+the function @code{transient--get-layout}.
@end itemize
These functions operate on the information stored in the
-@code{transient--layout} property of the @var{PREFIX} symbol. Suffix entries in
-that tree are not objects but have the form @code{(@var{LEVEL} @var{CLASS} @var{PLIST})}, where
-@var{PLIST} should set at least @code{:key}, @code{:description} and @code{:command}.
+@code{transient--layout} property of the @var{PREFIX} symbol. Elements in that
+tree are not objects but have the form @code{(@var{CLASS} @var{PLIST}) for suffixes} and
+[CLASS PLIST CHILDREN] for groups. At the root of the tree is an
+element [N nil CHILDREN], where N is the version of the layout format,
+currently and hopefully for a long time 2. While that element looks
+like a group vector, that element does not count when identifying a
+group using a coordinate vector, i.e., [0] is its first child, not the
+root element itself.
@defun transient-insert-suffix prefix loc suffix &optional keep-other
@end defun
they are never active at the same time, see @ref{Predicate Slots}.
Unfortunately both false-positives and false-negatives are possible.
-To deal with the former use non-@code{nil} @var{KEEP-OTHER@.} The symbol @code{always}
-prevents the removal of a false-positive in some cases where other
+To deal with the former, use non-@code{nil} @var{KEEP-OTHER@.} The symbol @code{always}
+prevents the removal of a false-positive, in some cases where other
non-@code{nil} values would fail. To deal with false-negatives remove the
conflicting binding separately, using @code{transient-remove-suffix}.
@end defun
the @var{PROP} of its plist to @var{VALUE}.
@end defun
+Some prefix commands share suffixes, which are separately and then
+included in each prefix when it is defined. The inclusion is done by
+reference, the included suffix groups are not inlined by default. So
+if you change, for example, the key binding for an argument in
+@code{magit-diff} (@code{d}) the same change also applies to @code{magit-diff-refresh} (@code{D}).
+In the rare case that this is not desirable use @code{transient-inline-group}
+before making changes to included suffixes.
+
+@defun transient-inline-group PREFIX GROUP
+This function inlines the included GROUP into PREFIX, by replacing
+the symbol GROUP with its expanded layout in the layout of PREFIX@.
+@end defun
+
Most of these functions do not signal an error if they cannot perform
the requested modification. The functions that insert new suffixes
show a warning if @var{LOC} cannot be found in @var{PREFIX} without signaling an
error. The reason for doing it like this is that establishing a key
binding (and that is what we essentially are trying to do here) should
not prevent the rest of the configuration from loading. Among these
-functions only @code{transient-get-suffix} and @code{transient-suffix-put} may
-signal an error.
+functions only @code{transient-get-suffix} and @code{transient-suffix-put} signal
+an error by default. If you really want the insert functions to also
+signal an error, set @code{transient-error-on-insert-failure} to @code{t}.
@node Defining New Commands
@chapter Defining New Commands
(temporary) keymap is activated, which binds the transient's infix and
suffix commands, and functions that control the transient state are
added to @code{pre-command-hook} and @code{post-command-hook}. The available suffix
-and infix commands and their state are shown in a popup buffer until
+and infix commands and their state are shown in a menu buffer until
the transient state is exited by invoking a suffix command.
Calling an infix command causes its value to be changed. How that is
the user, using the minibuffer.
Calling a suffix command usually causes the transient to be exited;
-the transient keymaps and hook functions are removed, the popup buffer
+the transient keymaps and hook functions are removed, the menu buffer
no longer shows information about the (no longer bound) suffix
commands, the values of some public global variables are set, while
some internal global variables are unset, and finally the command is
@cindex command dispatchers
Transient can be used to implement simple ``command dispatchers''. The
main benefit then is that the user can see all the available commands
-in a popup buffer, which can be thought of as a ``menu''. That is
-useful by itself because it frees the user from having to remember all
-the keys that are valid after a certain prefix key or command.
-Magit's @code{magit-dispatch} (on @kbd{C-x M-g}) command is an example of using
-Transient to merely implement a command dispatcher.
+in a temporarily shown buffer, which can be thought of as a ``menu''.
+That is useful by itself because it frees the user from having to
+remember all the keys that are valid after a certain prefix key or
+command. Magit's @code{magit-dispatch} (on @kbd{C-x M-g}) command is an example of
+using Transient to merely implement a command dispatcher.
In addition to that, Transient also allows users to interactively pass
arguments to commands. These arguments can be much more complex than
explicitly.
@var{GROUP}s add key bindings for infix and suffix commands and specify
-how these bindings are presented in the popup buffer. At least one
+how these bindings are presented in the menu buffer. At least one
@var{GROUP} has to be specified. @xref{Binding Suffix and Infix Commands}.
The @var{BODY} is optional. If it is omitted, then @var{ARGLIST} is ignored and
the branch whose variables are being configured.
@end defmac
+Sometimes multiple prefix commands share a common set of suffixes.
+For example, while @code{magit-diff} (@code{d}) and @code{magit-diff-refresh} (@code{D}) offer
+different suffixes to actually create or update a diff, they both
+offer the same infix arguments to control how that diff is formatted.
+Such shared groups should be defined using @code{transient-define-group}
+and then included in multiple prefixes, by using the symbol that
+identifies the group in the prefix definition, in a location where
+you would otherwise use a group vector. If an included group is
+placed at the top-level of a prefix (as opposed of inside inside
+a vector as a child group), then the symbol should be quoted.
+
+@defmac transient-define-group name group@dots{}
+This macro define one or more groups and stores them in symbol NAME@.
+GROUPs have the same form as for @code{transient-define-prefix}.
+@end defmac
+
@node Binding Suffix and Infix Commands
@section Binding Suffix and Infix Commands
@xref{Enabling and Disabling Suffixes}.
@item
-@var{KEY} is the key binding, either a vector or key description string.
+KEY is the key binding, a string in the format returned by
+@code{describe-key} and understood by @code{kbd}.
+
+That format is more permissive than the one accepted by @code{key-valid-p}.
+Being more permissive makes it possible, for example, to write the
+key binding, which toggles the @code{-a} command line argument, as "-a",
+instead of having to write "- a". Likewise additional spaces can be
+added, which is not removed when displaying the binding in the menu,
+which is useful for alignment purposes.
@item
@var{DESCRIPTION} is the description, either a string or a function that
also be specified using @code{:shortarg}) and the second as the long argument
(which can also be specified using @code{:argument}).
-Only the long argument is displayed in the popup buffer. See
+Only the long argument is displayed in the menu buffer. See
@code{transient-detect-key-conflicts} for how the short argument may be
used.
@defun transient--insert-group group
This generic function formats the group and its elements and inserts
the result into the current buffer, which is a temporary buffer.
-The contents of that buffer are later inserted into the popup buffer.
+The contents of that buffer are later inserted into the menu buffer.
Functions that are called by this function may need to operate in
the buffer from which the transient was called. To do so they can
take these form:
@lisp
-([LEVEL] :info DESCRIPTION [KEYWORD VALUE]...)
-([LEVEL] :info* DESCRIPTION [KEYWORD VALUE]...)
+(:info DESCRIPTION [KEYWORD VALUE]...)
+(:info* DESCRIPTION [KEYWORD VALUE]...)
@end lisp
The @code{:info} and @code{:info*} keyword arguments replaces the @code{:description}
it is somewhat likely that future improvements won't be fully
backward compatible.
+@item
+The @code{transient-cons-option} class is intended for situations where
+@code{transient-args} should return an alist, instead of a list of strings
+(arguments). Such suffixes can be specified in prefix definitions
+like so:
+
+@lisp
+(:cons OPTION :key KEY [KEYWORD VALUE]...)
+@end lisp
+
+OPTION may be something other than a string, likely a keyword or
+some other symbol, it is used as the @code{car} of the cons-cell. When
+using such an inline definition @code{:key} has to be specified. In most
+cases @code{:reader} should also be specified. When defining such a suffix
+separately, the "alist key" has to be specified using the @code{:variable}
+keyword argument.
+
+This class is still experimental it is somewhat likely that future
+improvements won't be fully backward compatible.
+
@item
The @code{transient-describe-target} class is used by the command
@code{transient-describe}.
@itemize
@item
-@code{key} The key, a key vector or a key description string.
+@code{key} is the key binding, a string in the format returned by
+@code{describe-key} and understood by @code{kbd}.
+
+That format is more permissive than the one accepted by @code{key-valid-p}.
+Being more permissive makes it possible, for example, to write the
+key binding, which toggles the @code{-a} command line argument, as "-a",
+instead of having to write "- a". Likewise additional spaces can be
+added, which is not removed when displaying the binding in the menu,
+which is useful for alignment purposes.
@item
@code{command} The command, a symbol.
@code{transient} Whether to stay transient. @xref{Transient State}.
@item
-@code{format} The format used to display the suffix in the popup buffer.
+@code{format} The format used to display the suffix in the menu buffer.
It must contain the following %-placeholders:
@itemize
@node FAQ
@appendix FAQ
-@anchor{Can I control how the popup buffer is displayed?}
-@appendixsec Can I control how the popup buffer is displayed?
+@anchor{Can I control how the menu buffer is displayed?}
+@appendixsec Can I control how the menu buffer is displayed?
Yes, see @code{transient-display-buffer-action} in @ref{Configuration}. You can
-also control how the popup buffer is displayed on a case-by-case basis
+also control how the menu buffer is displayed on a case-by-case basis
by passing @code{:display-action} to @code{transient-define-prefix}.
-@anchor{How can I copy text from the popup buffer?}
-@appendixsec How can I copy text from the popup buffer?
+@anchor{How can I copy text from the menu buffer?}
+@appendixsec How can I copy text from the menu buffer?
-To be able to mark text in Transient's popup buffer using the mouse,
+To be able to mark text in Transient's menu buffer using the mouse,
you have to add the below binding. Note that for technical reasons,
the region won't be visualized, while doing so. After you have quit
-the transient popup, you will be able to yank it in another buffer.
+the transient menu, you will be able to yank it in another buffer.
@lisp
(keymap-set transient-predicate-map
See @uref{https://github.com/magit/transient/wiki/Comparison-with-other-packages}.
-@anchor{Why did some of the key bindings change?}
-@appendixsec Why did some of the key bindings change?
-
-You may have noticed that the bindings for some of the common commands
-do @strong{not} have the prefix @kbd{C-x} and that furthermore some of these commands
-are grayed out while others are not. That unfortunately is a bit
-confusing if the section of common commands is not shown permanently,
-making the following explanation necessary.
-
-The purpose of usually hiding that section but showing it after the
-user pressed the respective prefix key is to conserve space and not
-overwhelm users with too much noise, while allowing the user to
-quickly list common bindings on demand.
-
-That however should not keep us from using the best possible key
-bindings. The bindings that do use a prefix do so to avoid wasting
-too many non-prefix bindings, keeping them available for use in
-individual transients. The bindings that do not use a prefix and that
-are @strong{not} grayed out are very important bindings that are @strong{always}
-available, even when invoking the ``common command key prefix'' or @strong{any
-other} transient-specific prefix. The non-prefix keys that @strong{are} grayed
-out however, are not available when any incomplete prefix key sequence
-is active. They do not use the ``common command key prefix'' because it
-is likely that users want to invoke them several times in a row and
-e.g., @kbd{M-p M-p M-p} is much more convenient than @kbd{C-x M-p C-x M-p C-x M-p}.
-
-You may also have noticed that the ``Set'' command is bound to @kbd{C-x s},
-while Magit-Popup used to bind @kbd{C-c C-c} instead. I have seen several
-users praise the latter binding (sic), so I did not change it
-willy-nilly. The reason that I changed it is that using different
-prefix keys for different common commands, would have made the
-temporary display of the common commands even more confusing, i.e.,
-after pressing @kbd{C-c} all the bindings that begin with the @kbd{C-x} prefix
-would be grayed out.
-
-Using a single prefix for common commands key means that all other
-potential prefix keys can be used for transient-specific commands
-@strong{without} the section of common commands also popping up. @kbd{C-c} in
-particular is a prefix that I want to (and already do) use for Magit, and
-also using that for a common command would prevent me from doing so.
-
-(See also the next question.)
-
@anchor{Why does @kbd{q} not quit popups anymore?}
@appendixsec Why does @kbd{q} not quit popups anymore?
;; Author: Jonas Bernoulli <jonas@bernoul.li>
;; URL: https://github.com/magit/transient
;; Keywords: extensions
-;; Version: 0.8.6
+;; Version: 0.9.1
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Code:
-(defconst transient-version "v0.8.6-7-g64cb8404-builtin")
+(defconst transient-version "v0.9.1-7-gd7d2c1c2-builtin")
(require 'cl-lib)
(require 'eieio)
(make-obsolete-variable 'transient-hide-during-minibuffer-read
'transient-show-during-minibuffer-read "0.8.0")
+(defvar transient-common-command-prefix)
+
(defmacro transient--with-emergency-exit (id &rest body)
(declare (indent defun))
(unless (keywordp id)
:group 'extensions)
(defcustom transient-show-popup t
- "Whether to show the current transient in a popup buffer.
+ "Whether and when to show transient's menu in a buffer.
\\<transient-map>
-- If t, then show the popup as soon as a transient prefix command
+- If t, then show the buffer as soon as a transient prefix command
is invoked.
-- If nil, then do not show the popup unless the user explicitly
+- If nil, then do not show the buffer unless the user explicitly
requests it, by pressing \\[transient-show] or a prefix key.
-- If a number, then delay displaying the popup and instead show
+- If a number, then delay displaying the buffer and instead show
a brief one-line summary. If zero or negative, then suppress
even showing that summary and display the pressed key only.
- Show the popup when the user explicitly requests it by pressing
- \\[transient-show] or a prefix key. Unless zero, then also show the popup
+ Show the buffer once the user explicitly requests it by pressing
+ \\[transient-show] or a prefix key. Unless zero, then also show the buffer
after that many seconds of inactivity (using the absolute value)."
:package-version '(transient . "0.1.0")
:group 'transient
- :type '(choice (const :tag "instantly" t)
- (const :tag "on demand" nil)
- (const :tag "on demand (no summary)" 0)
- (number :tag "after delay" 1)))
+ :type '(choice (const :tag "Instantly" t)
+ (const :tag "On demand" nil)
+ (const :tag "On demand (no summary)" 0)
+ (number :tag "After delay" 1)))
(defcustom transient-enable-popup-navigation 'verbose
- "Whether navigation commands are enabled in the transient popup.
+ "Whether navigation commands are enabled in the menu buffer.
If the value is `verbose', additionally show brief documentation
about the command under point in the echo area.
-While a transient is active the transient popup buffer is not the
+While a transient is active transient's menu buffer is not the
current buffer, making it necessary to use dedicated commands to
act on that buffer itself. If this is non-nil, then the following
bindings are available:
- \\`<mouse-1>' and \\`<mouse-2>' invoke the clicked on suffix.
\\<transient-popup-navigation-map>\
- \\[transient-isearch-backward]\
- and \\[transient-isearch-forward] start isearch in the popup buffer.
+ and \\[transient-isearch-forward] start isearch in the menu buffer.
\\`<mouse-1>' and \\`<mouse-2>' are bound in `transient-push-button'.
All other bindings are in `transient-popup-navigation-map'.
if no transient were active."
:package-version '(transient . "0.7.8")
:group 'transient
- :type '(choice (const :tag "enable navigation and echo summary" verbose)
- (const :tag "enable navigation commands" t)
- (const :tag "disable navigation commands" nil)))
+ :type '(choice (const :tag "Enable navigation and echo summary" verbose)
+ (const :tag "Enable navigation commands" t)
+ (const :tag "Disable navigation commands" nil)))
(defcustom transient-display-buffer-action
'(display-buffer-in-side-window
(side . bottom)
(dedicated . t)
(inhibit-same-window . t))
- "The action used to display the transient popup buffer.
+ "The action used to display transient's menu buffer.
-The transient popup buffer is displayed in a window using
+The transient menu buffer is displayed in a window using
(display-buffer BUFFER transient-display-buffer-action)
:type 'natnum)
(defcustom transient-mode-line-format 'line
- "The mode-line format for the transient popup buffer.
+ "The mode-line format for transient's menu buffer.
If nil, then the buffer has no mode-line. If the buffer is not
displayed right above the echo area, then this probably is not
See `mode-line-format' for details."
:package-version '(transient . "0.2.0")
:group 'transient
- :type '(choice (const :tag "hide mode-line" nil)
- (const :tag "substitute thin line" line)
- (number :tag "substitute line with thickness")
- (const :tag "name of prefix command"
+ :type '(choice (const :tag "Hide mode-line" nil)
+ (const :tag "Substitute thin line" line)
+ (number :tag "Substitute line with thickness")
+ (const :tag "Name of prefix command"
("%e" mode-line-front-space
mode-line-buffer-identification))
- (sexp :tag "custom mode-line format")))
+ (sexp :tag "Custom mode-line format")))
(defcustom transient-show-common-commands nil
- "Whether to show common transient suffixes in the popup buffer.
-
-These commands are always shown after typing the prefix key
-\\`C-x' when a transient command is active. To toggle the value
-of this variable use \\`C-x t' when a transient is active."
+ "Whether to permanently show common suffix commands in transient menus.
+
+By default these commands are only temporarily shown after typing their
+shared prefix key \
+\\<transient--docstr-hint-1>\\[transient-common-command-prefix], \
+while a transient menu is active. When the value
+of this option is non-nil, then these commands are permanently shown.
+To toggle the value for the current Emacs session only type \
+\\<transient--docstr-hint-2>\\[transient-toggle-common] while
+any transient menu is active."
:package-version '(transient . "0.1.0")
:group 'transient
:type 'boolean)
If non-nil, then the key binding of each suffix is colorized to
indicate whether it exits the transient state or not, and the
-line that is drawn below the transient popup buffer is used to
+line that is drawn below transient's menu buffer is used to
indicate the behavior of non-suffix commands."
:package-version '(transient . "0.5.0")
:group 'transient
:group 'transient
:type 'boolean)
+(defcustom transient-error-on-insert-failure nil
+ "Whether to signal an error when failing to insert a suffix.
+
+When `transient-insert-suffix' and `transient-append-suffix' fail
+to insert a suffix into an existing prefix, they usually just show
+a warning. If this is non-nil, they signal an error instead."
+ :package-version '(transient . "0.8.8")
+ :group 'transient
+ :type 'boolean)
+
(defcustom transient-align-variable-pitch nil
- "Whether to align columns pixel-wise in the popup buffer.
+ "Whether to align columns pixel-wise in the menu buffer.
If this is non-nil, then columns are aligned pixel-wise to
support variable-pitch fonts. Keys are not aligned, so you
:type 'boolean)
(defcustom transient-force-fixed-pitch nil
- "Whether to force use of monospaced font in the popup buffer.
+ "Whether to force use of monospaced font in the menu buffer.
Even if you use a proportional font for the `default' face,
you might still want to use a monospaced font in transient's
-popup buffer. Setting this option to t causes `default' to
+menu buffer. Setting this option to t causes `default' to
be remapped to `fixed-pitch' in that buffer.
See also `transient-align-variable-pitch'."
The levels of individual transients and/or their individual
suffixes can be changed individually, by invoking the prefix and
-then pressing \\`C-x l'.
+then pressing \\<transient--docstr-hint-2>\\[transient-set-level].
The default level for both transients and their suffixes is 4.
This option only controls the default for transients. The default
explicitly.
GROUPs add key bindings for infix and suffix commands and specify
-how these bindings are presented in the popup buffer. At least
+how these bindings are presented in the menu buffer. At least
one GROUP has to be specified. See info node `(transient)Binding
Suffix and Infix Commands'.
(indent defun)
(doc-string 3))
(pcase-let
- ((`(,class ,slots ,suffixes ,docstr ,body ,interactive-only)
+ ((`(,class ,slots ,groups ,docstr ,body ,interactive-only)
(transient--expand-define-args args arglist 'transient-define-prefix)))
`(progn
(defalias ',name
(put ',name 'function-documentation ,docstr)
(put ',name 'transient--prefix
(,(or class 'transient-prefix) :command ',name ,@slots))
- (put ',name 'transient--layout
- (list ,@(mapcan (lambda (s) (transient--parse-child name s))
- suffixes))))))
+ (transient--set-layout
+ ',name
+ (list ,@(mapcan (lambda (s) (transient--parse-child name s)) groups))))))
+
+(defmacro transient-define-group (name &rest groups)
+ "Define one or more groups and store them in symbol NAME.
+
+Groups defined using this macro, can be used inside the
+definition of transient prefix commands, by using the symbol
+NAME where a group vector is expected. GROUPS has the same
+form as for `transient-define-prefix'."
+ (declare (debug (&define name [&rest vectorp]))
+ (indent defun))
+ `(transient--set-layout
+ ',name
+ (list ,@(mapcan (lambda (s) (transient--parse-child name s)) groups))))
(defmacro transient-define-suffix (name arglist &rest args)
"Define NAME as a transient suffix command.
;; ARGLIST and FORM are only optional for backward compatibility.
;; This is necessary because "emoji.el" from Emacs 29 calls this
;; function directly, with just one argument.
+ (declare (advertised-calling-convention
+ (args arglist form &optional nobody) "0.7.1"))
(unless (listp arglist)
(error "Mandatory ARGLIST is missing"))
(let (class keys suffixes docstr declare (interactive-only t))
(setq class v)
(push k keys)
(push v keys))))
- (while (let ((arg (car args)))
- (or (vectorp arg)
- (and arg (symbolp arg))))
- (push (pop args) suffixes))
+ (while-let
+ ((arg (car args))
+ (arg (cond
+ ;; Inline group definition.
+ ((vectorp arg)
+ (pop args))
+ ;; Quoted include, as one would expect.
+ ((eq (car-safe arg) 'quote)
+ (cadr (pop args)))
+ ;; Unquoted include, for compatibility.
+ ((and arg (symbolp arg))
+ (pop args)))))
+ (push arg suffixes))
(when (eq (car-safe (car args)) 'declare)
(setq declare (car args))
(setq args (cdr args))
(defun transient--parse-child (prefix spec)
(cl-typecase spec
(null (error "Invalid transient--parse-child spec: %s" spec))
- (symbol (let ((value (symbol-value spec)))
- (if (and (listp value)
- (or (listp (car value))
- (vectorp (car value))))
- (mapcan (lambda (s) (transient--parse-child prefix s)) value)
- (transient--parse-child prefix value))))
+ (symbol (list `',spec))
(vector (and-let* ((c (transient--parse-group prefix spec))) (list c)))
(list (and-let* ((c (transient--parse-suffix prefix spec))) (list c)))
(string (list spec))
(t (error "Invalid transient--parse-child spec: %s" spec))))
(defun transient--parse-group (prefix spec)
- (let ((spec (append spec nil))
- level class args)
+ (let (class args)
+ (setq spec (append spec nil))
(when (integerp (car spec))
- (setq level (pop spec)))
+ (setq args (plist-put args :level (pop spec))))
(when (stringp (car spec))
(setq args (plist-put args :description (pop spec))))
- ;; Merge value of [... GROUP-VARIABLE], if any.
- (let ((spec* spec))
- (while (keywordp (car spec*))
- (setq spec* (cddr spec*)))
- (when (and (length= spec* 1) (symbolp (car spec*)))
- (let ((rest (append (symbol-value (car spec*)) nil))
- (args nil))
- (while (keywordp (car rest))
- (setq args (nconc (list (pop rest) (pop rest)) args)))
- (setq spec (nconc args (butlast spec) rest)))))
(while (keywordp (car spec))
(let* ((key (pop spec))
(val (if spec (pop spec) (error "No value for `%s'" key))))
(message "WARNING: %s: When %s is used, %s must also be specified"
'transient-define-prefix :setup-children :class))
(list 'vector
- level
(list 'quote
(cond (class)
((cl-typep (car spec)
(mapcan (lambda (s) (transient--parse-child prefix s)) spec)))))
(defun transient--parse-suffix (prefix spec)
- (let (level class args)
+ (let (class args)
(cl-flet ((use (prop value)
(setq args (plist-put args prop value))))
(pcase (car spec)
((cl-type integer)
- (setq level (pop spec))))
+ (use :level (pop spec))))
(pcase (car spec)
((cl-type (or string vector))
(use :key (pop spec))))
(guard (commandp (cadr spec))))
(use :description (macroexp-quote (pop spec)))))
(pcase (car spec)
- ((or :info :info*))
+ ((or :info :info* :cons))
((and (cl-type keyword) invalid)
- (error "Need command, argument, `:info' or `:info*'; got `%s'" invalid))
+ (error "Need command, argument, `:info', `:info*' or `:cons'; got `%s'"
+ invalid))
((cl-type symbol)
(use :command (macroexp-quote (pop spec) 'function)))
;; During macro-expansion this is expected to be a `lambda'
(val (if spec (pop spec) (error "No value for `%s'" key))))
(pcase key
(:class (setq class val))
- (:level (setq level val))
(:info (setq class 'transient-information)
(use :description val))
(:info* (setq class 'transient-information*)
(use :description val))
+ (:cons
+ (setq class 'transient-cons-option)
+ (use :command
+ (let ((sym (intern (format "transient:%s:%s" prefix val))))
+ `(prog1 ',sym
+ (put ',sym 'interactive-only t)
+ (put ',sym 'completion-predicate #'transient--suffix-only)
+ (defalias ',sym #'transient--default-infix-command))))
+ (use :argument val))
((guard (eq (car-safe val) '\,))
(use key (cadr val)))
((guard (or (symbolp val)
(_ (use key val)))))
(when spec
(error "Need keyword, got %S" (car spec)))
- (when-let* (((not (plist-get args :key)))
- (shortarg (plist-get args :shortarg)))
- (use :key shortarg)))
- (list 'list
- level
+ (if-let* ((key (plist-get args :key)))
+ (when (string-match "\\`\\({p}\\)" key)
+ (use :key
+ (replace-match transient-common-command-prefix t t key 1)))
+ (when-let* ((shortarg (plist-get args :shortarg)))
+ (use :key shortarg))))
+ (list 'cons
(macroexp-quote (or class 'transient-suffix))
(cons 'list args))))
(setq read-extended-command-predicate
#'transient-command-completion-not-suffix-only-p))
+(defun transient--set-layout (prefix layout)
+ (put prefix 'transient--layout (vector 2 nil layout)))
+
+(defun transient--get-layout (prefix)
+ (if-let*
+ ((layout
+ (or (get prefix 'transient--layout)
+ ;; Migrate unparsed legacy group definition.
+ (condition-case-unless-debug err
+ (and-let* ((value (symbol-value prefix)))
+ (transient--set-layout
+ prefix
+ (if (and (listp value)
+ (or (listp (car value))
+ (vectorp (car value))))
+ (transient-parse-suffixes prefix value)
+ (list (transient-parse-suffix prefix value)))))
+ (error
+ (message "Not a legacy group definition: %s: %S" prefix err)
+ nil)))))
+ (if (vectorp layout)
+ (let ((version (aref layout 0)))
+ (if (= version 2)
+ layout
+ (error "Unsupported layout version %s for %s" version prefix)))
+ ;; Upgrade from version 1.
+ (cl-labels
+ ((upgrade (spec)
+ (cond
+ ((vectorp spec)
+ (pcase-let ((`[,level ,class ,args ,children] spec))
+ (when level
+ (setq args (plist-put args :level level)))
+ (vector class args (mapcar #'upgrade children))))
+ ((and (listp spec)
+ (length= spec 3)
+ (or (null (car spec))
+ (natnump (car spec)))
+ (symbolp (cadr spec)))
+ (pcase-let ((`(,level ,class ,args) spec))
+ (when level
+ (setq args (plist-put args :level level)))
+ (cons class args)))
+ ((listp spec)
+ (mapcar #'upgrade spec))
+ (t spec))))
+ (transient--set-layout prefix (upgrade layout))))
+ (error "Not a transient prefix command or group definition: %s" prefix)))
+
+(defun transient--get-children (prefix)
+ (aref (transient--get-layout prefix) 2))
+
(defun transient-parse-suffix (prefix suffix)
"Parse SUFFIX, to be added to PREFIX.
-PREFIX is a prefix command, a symbol.
+PREFIX is a prefix command symbol or object.
SUFFIX is a suffix command or a group specification (of
the same forms as expected by `transient-define-prefix').
Intended for use in a group's `:setup-children' function."
- (cl-assert (and prefix (symbolp prefix)))
+ (when (cl-typep prefix 'transient-prefix)
+ (setq prefix (oref prefix command)))
(eval (car (transient--parse-child prefix suffix)) t))
(defun transient-parse-suffixes (prefix suffixes)
"Parse SUFFIXES, to be added to PREFIX.
-PREFIX is a prefix command, a symbol.
+PREFIX is a prefix command symbol or object.
SUFFIXES is a list of suffix command or a group specification
(of the same forms as expected by `transient-define-prefix').
Intended for use in a group's `:setup-children' function."
- (cl-assert (and prefix (symbolp prefix)))
+ (when (cl-typep prefix 'transient-prefix)
+ (setq prefix (oref prefix command)))
(mapcar (apply-partially #'transient-parse-suffix prefix) suffixes))
;;; Edit
(defun transient--insert-suffix (prefix loc suffix action &optional keep-other)
- (let* ((suf (cl-etypecase suffix
- (vector (transient--parse-group prefix suffix))
- (list (transient--parse-suffix prefix suffix))
- (string suffix)))
- (mem (transient--layout-member loc prefix))
- (elt (car mem)))
- (setq suf (eval suf t))
+ (pcase-let* ((suf (cl-etypecase suffix
+ (vector (eval (transient--parse-group prefix suffix) t))
+ (list (eval (transient--parse-suffix prefix suffix) t))
+ (string suffix)
+ (symbol suffix)))
+ (`(,elt ,group) (transient--locate-child prefix loc)))
(cond
- ((not mem)
- (message "Cannot insert %S into %s; %s not found"
+ ((not elt)
+ (funcall (if transient-error-on-insert-failure #'error #'message)
+ "Cannot insert %S into %s; %s not found"
suffix prefix loc))
((or (and (vectorp suffix) (not (vectorp elt)))
(and (listp suffix) (vectorp elt))
(and (stringp suffix) (vectorp elt)))
- (message "Cannot place %S into %s at %s; %s"
+ (funcall (if transient-error-on-insert-failure #'error #'message)
+ "Cannot place %S into %s at %s; %s"
suffix prefix loc
"suffixes and groups cannot be siblings"))
(t
(when-let* (((not (eq keep-other 'always)))
(bindingp (listp suf))
- (key (transient--spec-key suf))
- (conflict (car (transient--layout-member key prefix)))
+ (key (transient--suffix-key suf))
+ (conflict (car (transient--locate-child prefix key)))
(conflictp
(and (not (and (eq action 'replace)
(eq conflict elt)))
(or (not keep-other)
- (eq (plist-get (nth 2 suf) :command)
- (plist-get (nth 2 conflict) :command)))
+ (eq (plist-get (transient--suffix-props suf)
+ :command)
+ (plist-get (transient--suffix-props conflict)
+ :command)))
(equal (transient--suffix-predicate suf)
(transient--suffix-predicate conflict)))))
(transient-remove-suffix prefix key))
- (pcase-exhaustive action
- ('insert (setcdr mem (cons elt (cdr mem)))
- (setcar mem suf))
- ('append (setcdr mem (cons suf (cdr mem))))
- ('replace (setcar mem suf)))))))
+ (let ((mem (memq elt (aref group 2))))
+ (pcase-exhaustive action
+ ('insert (setcdr mem (cons elt (cdr mem)))
+ (setcar mem suf))
+ ('append (setcdr mem (cons suf (cdr mem))))
+ ('replace (setcar mem suf))))))))
;;;###autoload
(defun transient-insert-suffix (prefix loc suffix &optional keep-other)
(declare (indent defun))
(transient--insert-suffix prefix loc suffix 'replace))
+;;;###autoload
+(defun transient-inline-group (prefix group)
+ "Inline the included GROUP into PREFIX.
+Replace the symbol GROUP with its expanded layout in the
+layout of PREFIX."
+ (declare (indent defun))
+ (cl-assert (symbolp group))
+ (pcase-let ((`(,suffix ,parent) (transient--locate-child prefix group)))
+ (when suffix
+ (let* ((siblings (aref parent 2))
+ (pos (cl-position group siblings)))
+ (aset parent 2
+ (nconc (seq-take siblings pos)
+ (transient--get-children group)
+ (seq-drop siblings (1+ pos))))))))
+
;;;###autoload
(defun transient-remove-suffix (prefix loc)
"Remove the suffix or group at LOC in PREFIX.
(whose last element may also be a command or key).
See info node `(transient)Modifying Existing Transients'."
(declare (indent defun))
- (transient--layout-member loc prefix 'remove))
+ (pcase-let ((`(,suffix ,group) (transient--locate-child prefix loc)))
+ (when suffix
+ (aset group 2 (delq suffix (aref group 2))))))
-(defun transient-get-suffix (prefix loc)
- "Return the suffix or group at LOC in PREFIX.
+(defun transient-suffix-put (prefix loc prop value)
+ "Edit the suffix at LOC in PREFIX, setting PROP to VALUE.
PREFIX is a prefix command, a symbol.
+SUFFIX is a suffix command or a group specification (of
+ the same forms as expected by `transient-define-prefix').
LOC is a command, a key vector, a key description (a string
as returned by `key-description'), or a coordination list
(whose last element may also be a command or key).
See info node `(transient)Modifying Existing Transients'."
- (if-let* ((mem (transient--layout-member loc prefix)))
- (car mem)
- (error "%s not found in %s" loc prefix)))
+ (let ((child (transient-get-suffix prefix loc)))
+ (if (vectorp child)
+ (aset child 1 (plist-put (aref child 1) prop value))
+ (setcdr child (plist-put (transient--suffix-props child) prop value)))))
-(defun transient-suffix-put (prefix loc prop value)
- "Edit the suffix at LOC in PREFIX, setting PROP to VALUE.
+(defalias 'transient--suffix-props #'cdr)
+
+(defun transient-get-suffix (prefix loc)
+ "Return the suffix or group at LOC in PREFIX.
PREFIX is a prefix command, a symbol.
-SUFFIX is a suffix command or a group specification (of
- the same forms as expected by `transient-define-prefix').
LOC is a command, a key vector, a key description (a string
as returned by `key-description'), or a coordination list
(whose last element may also be a command or key).
See info node `(transient)Modifying Existing Transients'."
- (let ((suf (transient-get-suffix prefix loc)))
- (setf (elt suf 2)
- (plist-put (elt suf 2) prop value))))
-
-(defun transient--layout-member (loc prefix &optional remove)
- (let ((val (or (get prefix 'transient--layout)
- (error "%s is not a transient command" prefix))))
- (when (listp loc)
- (while (integerp (car loc))
- (let* ((children (if (vectorp val) (aref val 3) val))
- (mem (transient--nthcdr (pop loc) children)))
- (if (and remove (not loc))
- (let ((rest (delq (car mem) children)))
- (if (vectorp val)
- (aset val 3 rest)
- (put prefix 'transient--layout rest))
- (setq val nil))
- (setq val (if loc (car mem) mem)))))
- (setq loc (car loc)))
- (if loc
- (transient--layout-member-1 (transient--kbd loc) val remove)
- val)))
-
-(defun transient--layout-member-1 (loc layout remove)
- (cond ((listp layout)
- (seq-some (lambda (elt) (transient--layout-member-1 loc elt remove))
- layout))
- ((vectorp (car (aref layout 3)))
- (seq-some (lambda (elt) (transient--layout-member-1 loc elt remove))
- (aref layout 3)))
- (remove
- (aset layout 3
- (delq (car (transient--group-member loc layout))
- (aref layout 3)))
- nil)
- ((transient--group-member loc layout))))
-
-(defun transient--group-member (loc group)
- (cl-member-if (lambda (suffix)
- (and (listp suffix)
- (let* ((def (nth 2 suffix))
- (cmd (plist-get def :command)))
- (if (symbolp loc)
- (eq cmd loc)
- (equal (transient--kbd
- (or (plist-get def :key)
- (transient--command-key cmd)))
- loc)))))
- (aref group 3)))
-
-(defun transient--kbd (keys)
- (when (vectorp keys)
- (setq keys (key-description keys)))
- (when (stringp keys)
- (setq keys (kbd keys)))
- keys)
-
-(defun transient--spec-key (spec)
- (let ((plist (nth 2 spec)))
- (or (plist-get plist :key)
+ (or (car (transient--locate-child prefix loc))
+ (error "%s not found in %s" loc prefix)))
+
+(defun transient--locate-child (group loc)
+ (when (symbolp group)
+ (setq group (transient--get-layout group)))
+ (when (vectorp loc)
+ (setq loc (append loc nil)))
+ (if (listp loc)
+ (and-let* ((match (transient--nth (pop loc) (aref group 2))))
+ (if loc
+ (transient--locate-child
+ match (cond ((or (stringp (car loc))
+ (symbolp (car loc)))
+ (car loc))
+ ((symbolp match)
+ (vconcat (cons 0 loc)))
+ ((vconcat loc))))
+ (list match group)))
+ (seq-some (lambda (child)
+ (transient--match-child group loc child))
+ (aref group 2))))
+
+(defun transient--match-child (group loc child)
+ (cl-etypecase child
+ (string nil)
+ (symbol (if (symbolp loc)
+ (and (eq child loc)
+ (list child group))
+ (and-let* ((include (transient--get-layout child)))
+ (transient--locate-child include loc))))
+ (vector (seq-some (lambda (subgroup)
+ (transient--locate-child subgroup loc))
+ (aref group 2)))
+ (list (and (if (symbolp loc)
+ (eq (plist-get (transient--suffix-props child) :command)
+ loc)
+ (equal (kbd (transient--suffix-key child))
+ (kbd loc)))
+ (list child group)))))
+
+(defun transient--nth (n list)
+ (nth (if (< n 0) (- (length list) (abs n)) n) list))
+
+(defun transient--suffix-key (spec)
+ (let ((props (transient--suffix-props spec)))
+ (or (plist-get props :key)
(transient--command-key
- (plist-get plist :command)))))
+ (plist-get props :command)))))
(defun transient--command-key (cmd)
(and-let* ((obj (transient--suffix-prototype cmd)))
(oref obj shortarg)
(transient--derive-shortarg (oref obj argument)))))))
-(defun transient--nthcdr (n list)
- (nthcdr (if (< n 0) (- (length list) (abs n)) n) list))
-
(defun transient-set-default-level (command level)
"Set the default level of suffix COMMAND to LEVEL.
variable instead.")
(defvar transient-exit-hook nil
- "Hook run after exiting a transient.")
+ "Hook run after exiting a transient menu.
+Unlike `transient-post-exit-hook', this runs even if another transient
+menu becomes active at the same time. ")
+
+(defvar transient-post-exit-hook nil
+ "Hook run after exiting all transient menus.
+Unlike `transient-exit-hook', this does not run if another transient
+menu becomes active at the same time.")
(defvar transient-setup-buffer-hook nil
"Hook run when setting up the transient buffer.
(defconst transient--exit nil "Do exit the transient.")
(defvar transient--exitp nil "Whether to exit the transient.")
-(defvar transient--showp nil "Whether to show the transient popup buffer.")
+(defvar transient--showp nil "Whether to show the transient menu buffer.")
(defvar transient--helpp nil "Whether help-mode is active.")
(defvar transient--docsp nil "Whether docstring-mode is active.")
(defvar transient--editp nil "Whether edit-mode is active.")
"The transient menu buffer.")
(defvar transient--window nil
- "The window used to display the transient popup buffer.")
+ "The window used to display transient's menu buffer.")
(defvar transient--original-window nil
"The window that was selected before the transient was invoked.
((length= suffixes 1)
(car suffixes))
((cl-find-if (lambda (obj)
- (equal
- (listify-key-sequence (transient--kbd (oref obj key)))
- (listify-key-sequence (this-command-keys))))
+ (equal (listify-key-sequence (kbd (oref obj key)))
+ (listify-key-sequence (this-command-keys))))
suffixes))
;; COMMAND is only provided if `this-command' is meaningless, in
;; which case `this-command-keys' is also meaningless, making it
;; If COMMAND is nil, then failure to disambiguate likely means
;; that there is a bug somewhere.
((length> suffixes 1)
- (error "BUG: Cannot unambigiously determine suffix object"))
+ (error "BUG: Cannot unambiguously determine suffix object"))
;; It is legimate to use this function as a predicate of sorts.
;; `transient--pre-command' and `transient-help' are examples.
(t nil))))
(keymap-set map "C-t" #'transient-show)
(keymap-set map "?" #'transient-help)
(keymap-set map "C-h" #'transient-help)
- ;; Also bound to "C-x p" and "C-x n" in transient-common-commands.
+ ;; Next two have additional bindings in transient-common-commands.
(keymap-set map "C-M-p" #'transient-history-prev)
(keymap-set map "C-M-n" #'transient-history-next)
(when (fboundp 'other-frame-prefix) ;Emacs >= 28.1
(defvar-keymap transient-edit-map
:doc "Keymap that is active while a transient in is in \"edit mode\"."
:parent transient-base-map
- "?" #'transient-help
- "C-h" #'transient-help
- "C-x l" #'transient-set-level)
+ "?" #'transient-help
+ "C-h" #'transient-help)
(defvar-keymap transient-sticky-map
:doc "Keymap that is active while an incomplete key sequence is active."
:parent transient-base-map
"C-g" #'transient-quit-seq)
-(defvar transient--common-command-prefixes '(?\C-x))
-
-(put 'transient-common-commands
- 'transient--layout
- (list
- (eval
- (car (transient--parse-child
- 'transient-common-commands
- (vector
- :hide
- (lambda ()
- (and (not (memq
- (car (bound-and-true-p transient--redisplay-key))
- transient--common-command-prefixes))
- (not transient-show-common-commands)))
- (vector
- "Value commands"
- (list "C-x s " "Set" #'transient-set)
- (list "C-x C-s" "Save" #'transient-save)
- (list "C-x C-k" "Reset" #'transient-reset)
- (list "C-x p " "Previous value" #'transient-history-prev)
- (list "C-x n " "Next value" #'transient-history-next))
- (vector
- "Sticky commands"
- ;; Like `transient-sticky-map' except that
- ;; "C-g" has to be bound to a different command.
- (list "C-g" "Quit prefix or transient" #'transient-quit-one)
- (list "C-q" "Quit transient stack" #'transient-quit-all)
- (list "C-z" "Suspend transient stack" #'transient-suspend))
- (vector
- "Customize"
- (list "C-x t" #'transient-toggle-common)
- (list "C-x l" "Show/hide suffixes" #'transient-set-level)
- (list "C-x a" #'transient-toggle-level-limit)))))
- t)))
+(defvar transient-common-commands
+ [:hide (lambda ()
+ (defvar transient--redisplay-key)
+ (and (not (equal (vconcat transient--redisplay-key)
+ (read-kbd-macro transient-common-command-prefix)))
+ (not transient-show-common-commands)))
+ ["Value commands"
+ ("{p} s " "Set" transient-set)
+ ("{p} C-s" "Save" transient-save)
+ ("{p} C-k" "Reset" transient-reset)
+ ("{p} p " "Previous value" transient-history-prev)
+ ("{p} n " "Next value" transient-history-next)]
+ ["Sticky commands"
+ ;; Like `transient-sticky-map' except that
+ ;; "C-g" has to be bound to a different command.
+ ("C-g" "Quit prefix or transient" transient-quit-one)
+ ("C-q" "Quit transient stack" transient-quit-all)
+ ("C-z" "Suspend transient stack" transient-suspend)]
+ ["Customize"
+ ("{p} t" transient-toggle-common)
+ ("{p} l" "Show/hide suffixes" transient-set-level)
+ ("{p} a" transient-toggle-level-limit)]]
+ "Commands available in all transient menus.
+
+The same functions, that are used to change bindings in transient prefix
+commands and transient groups (defined using `transient-define-group'),
+should be used to modify these bindings as well. The actual layout is
+stored in the symbol's `transient--layout' property. The variable value
+is only used when customizing `transient-common-command-prefix', which
+resets the value of `transient--layout' based on the values of that
+option and this variable.")
+
+(defun transient--init-common-commands ()
+ (transient--set-layout
+ 'transient-common-commands
+ (list (eval (car (transient--parse-child 'transient-common-commands
+ transient-common-commands))
+ t)))
+ (defvar transient-common-command-prefix)
+ (defvar transient--docstr-hint-1)
+ (defvar transient--docstr-hint-2)
+ (setq transient--docstr-hint-1
+ (define-keymap transient-common-command-prefix
+ 'transient-common-command-prefix))
+ (setq transient--docstr-hint-2
+ (define-keymap (concat transient-common-command-prefix " t")
+ 'transient-toggle-common)))
+
+(defcustom transient-common-command-prefix "C-x"
+ "The prefix key used for most commands common to all menus.
+
+Some shared commands are available in all transient menus, most of
+which share a common prefix specified by this option. By default the
+bindings for these shared commands are only shown after pressing that
+prefix key and before following that up with a valid key binding.
+
+For historic reasons \\`C-x' is used by default, but users are
+encouraged to pick another key, preferably one that is not commonly used
+in Emacs but is still convenient to them. See info node `(transient)
+Common Suffix Commands'."
+ :type 'key
+ :initialize (lambda (symbol exp)
+ (custom-initialize-default symbol exp)
+ (transient--init-common-commands))
+ :set (lambda (symbol value)
+ (set-default symbol value)
+ (transient--init-common-commands)))
(defvar-keymap transient-popup-navigation-map
- :doc "One of the keymaps used when popup navigation is enabled.
+ :doc "One of the keymaps used when menu navigation is enabled.
See `transient-enable-popup-navigation'."
"<down-mouse-1>" #'transient-noop
"<up>" #'transient-backward-button
"M-RET" #'transient-push-button)
(defvar-keymap transient-button-map
- :doc "One of the keymaps used when popup navigation is enabled.
+ :doc "One of the keymaps used when menu navigation is enabled.
See `transient-enable-popup-navigation'."
"<mouse-1>" #'transient-push-button
"<mouse-2>" #'transient-push-button)
(defun transient--make-transient-map ()
(let ((map (make-sparse-keymap)))
- (set-keymap-parent map (if transient--editp
- transient-edit-map
- transient-map))
+ (cond (transient--editp
+ (keymap-set map (concat transient-common-command-prefix " l")
+ #'transient-set-level)
+ (set-keymap-parent map transient-edit-map))
+ ((set-keymap-parent map transient-map)))
(dolist (obj transient--suffixes)
(let ((key (oref obj key)))
(when (vectorp key)
(setq transient--minibuffer-depth (minibuffer-depth))
(transient--redisplay))
(get name 'transient--prefix))
+ (transient--suspend-text-conversion-style)
(transient--setup-transient)
(transient--suspend-which-key-mode)))
(defun transient--init-suffixes (name)
(let ((levels (alist-get name transient-levels)))
(mapcan (lambda (c) (transient--init-child levels c nil))
- (append (get name 'transient--layout)
+ (append (transient--get-children name)
(and (not transient--editp)
- (get 'transient-common-commands
- 'transient--layout))))))
+ (transient--get-children 'transient-common-commands))))))
(defun transient--flatten-suffixes (layout)
(cl-labels ((s (def)
(defun transient--init-child (levels spec parent)
(cl-etypecase spec
+ (symbol (mapcan (lambda (c) (transient--init-child levels c parent))
+ (transient--get-children spec)))
(vector (transient--init-group levels spec parent))
(list (transient--init-suffix levels spec parent))
(string (list spec))))
(defun transient--init-group (levels spec parent)
- (pcase-let* ((`(,level ,class ,args ,children) (append spec nil))
- (level (or level transient--default-child-level)))
+ (pcase-let* ((`[,class ,args ,children] spec)
+ (level (or (plist-get args :level)
+ transient--default-child-level)))
(and-let* (((transient--use-level-p level))
(obj (apply class :parent parent :level level args))
((transient--use-suffix-p obj))
(list obj)))))
(defun transient--init-suffix (levels spec parent)
- (pcase-let* ((`(,level ,class ,args) spec)
+ (pcase-let* ((`(,class . ,args) spec)
(cmd (plist-get args :command))
- (key (transient--kbd (plist-get args :key)))
+ (_ (transient--load-command-if-autoload cmd))
+ (key (plist-get args :key))
+ (key (and key (kbd key)))
(proto (and cmd (transient--suffix-prototype cmd)))
(level (or (alist-get (cons cmd key) levels nil nil #'equal)
(alist-get cmd levels)
- level
+ (plist-get args :level)
(and proto (oref proto level))
transient--default-child-level)))
- (transient--load-command-if-autoload cmd)
(when (transient--use-level-p level)
(let ((obj (if (child-of-class-p class 'transient-information)
(apply class :parent parent :level level args)
(if (transient-switches--eieio-childp obj)
(cl-call-next-method obj)
(when-let* (((not (slot-boundp obj 'shortarg)))
- (shortarg (transient--derive-shortarg (oref obj argument))))
+ (argument (oref obj argument))
+ ((stringp argument))
+ (shortarg (transient--derive-shortarg argument)))
(oset obj shortarg shortarg))
(unless (slot-boundp obj 'key)
(if (slot-boundp obj 'shortarg)
(default)))
(defun transient--suffix-predicate (spec)
- (let ((plist (nth 2 spec)))
+ (let ((props (transient--suffix-props spec)))
(seq-some (lambda (prop)
- (and-let* ((pred (plist-get plist prop)))
+ (and-let* ((pred (plist-get props prop)))
(list prop pred)))
'( :if :if-not
:if-nil :if-non-nil
(add-hook 'pre-command-hook #'transient--pre-command 99)
(add-hook 'post-command-hook #'transient--post-command)
(advice-add 'recursive-edit :around #'transient--recursive-edit)
+ (set-default-toplevel-value 'inhibit-quit t)
(when transient--exitp
;; This prefix command was invoked as the suffix of another.
;; Prevent `transient--post-command' from removing the hooks
(remove-hook 'pre-command-hook #'transient--pre-command)
(remove-hook 'post-command-hook #'transient--post-command)
(advice-remove 'recursive-edit #'transient--recursive-edit))
- (let ((resume (and transient--stack
+ (let ((replace (eq transient--exitp 'replace))
+ (resume (and transient--stack
(not (memq transient--exitp '(replace suspend))))))
- (unless (or resume (eq transient--exitp 'replace))
+ (unless (or resume replace)
(setq transient--showp nil))
(setq transient--exitp nil)
(setq transient--helpp nil)
(setq transient-current-command nil)
(setq transient-current-suffixes nil)
(setq transient--current-suffix nil))
- (when resume
- (transient--stack-pop))))
+ (cond (resume (transient--stack-pop))
+ ((not replace)
+ (setq quit-flag nil)
+ (set-default-toplevel-value 'inhibit-quit nil)
+ (run-hooks 'transient-post-exit-hook)))))
(defun transient--stack-push ()
(transient--debug 'stack-push)
(defun transient--emergency-exit (&optional id)
"Exit the current transient command after an error occurred.
When no transient is active (i.e., when `transient--prefix' is
-nil) then do nothing. Optional ID is a keyword identifying the
-exit."
+nil) then only reset `inhibit-quit'. Optional ID is a keyword
+identifying the exit."
(transient--debug 'emergency-exit id)
+ (set-default-toplevel-value 'inhibit-quit nil)
(when transient--prefix
(setq transient--stack nil)
(setq transient--exitp t)
Please open an issue and post the shown command log." :error)))
(defun transient-inhibit-move ()
- "Warn the user that popup navigation is disabled."
+ "Warn the user that menu navigation is disabled."
(interactive)
(message "To enable use of `%s', please customize `%s'"
this-original-command
(interactive))
(defun transient-update ()
- "Redraw the transient's state in the popup buffer."
+ "Redraw the transient's state in the menu buffer."
(interactive)
(setq prefix-arg current-prefix-arg))
(defun transient-show ()
- "Show the transient's state in the popup buffer."
+ "Show the transient's state in the menu buffer."
(interactive)
(setq transient--showp t))
:description "Echo arguments"
:key "x"
(interactive (list (transient-args transient-current-command)))
- (message "%s: %s"
- (key-description (this-command-keys))
- (mapconcat (lambda (arg)
- (propertize (if (string-match-p " " arg)
- (format "%S" arg)
- arg)
- 'face 'transient-argument))
- arguments " ")))
+ (if (seq-every-p #'stringp arguments)
+ (message "%s: %s" (key-description (this-command-keys))
+ (mapconcat (lambda (arg)
+ (propertize (if (string-match-p " " arg)
+ (format "%S" arg)
+ arg)
+ 'face 'transient-argument))
+ arguments " "))
+ (message "%s: %S" (key-description (this-command-keys)) arguments)))
;;; Value
;;;; Init
prompt)))
(if (stringp prompt)
prompt
- "(BUG: no prompt): "))
- (or (and-let* ((arg (and (slot-boundp obj 'argument) (oref obj argument))))
- (if (and (stringp arg) (string-suffix-p "=" arg))
- arg
- (concat arg ": ")))
- (and-let* ((var (and (slot-boundp obj 'variable) (oref obj variable))))
- (and (stringp var)
- (concat var ": ")))
- "(BUG: no prompt): ")))
+ "[BUG: invalid prompt]: "))
+ (if-let* ((name (or (and (slot-boundp obj 'argument) (oref obj argument))
+ (and (slot-boundp obj 'variable) (oref obj variable)))))
+ (if (and (stringp name)
+ (string-suffix-p "=" name))
+ name
+ (format "%s: " name))
+ "[BUG: no prompt]: ")))
;;;; Set
'transient-inactive-argument)))
(cl-defmethod transient-format-value ((obj transient-option))
- (let ((argument (oref obj argument)))
+ (let ((argument (prin1-to-string (oref obj argument) t)))
(if-let* ((value (oref obj value)))
(pcase-exhaustive (oref obj multi-value)
('nil
(propertize "<KEY>" 'face 'transient-key)
(propertize "<N>" 'face 'transient-key)
(propertize " N " 'face 'transient-enabled-suffix)
- (propertize "C-x l" 'face 'transient-key)
+ (propertize (concat transient-common-command-prefix " l")
+ 'face 'transient-key)
(propertize "<N>" 'face 'transient-key)
(propertize " N " 'face 'transient-enabled-suffix)
(propertize "C-h" 'face 'transient-key)
(let ((message-log-max nil))
(message "%s" doc))))))
-;;; Popup Navigation
+;;; Menu Navigation
(defun transient-scroll-up (&optional arg)
- "Scroll text of transient popup window upward ARG lines.
+ "Scroll text of transient's menu window upward ARG lines.
If ARG is nil scroll near full screen. This is a wrapper
around `scroll-up-command' (which see)."
(interactive "^P")
(scroll-up-command arg)))
(defun transient-scroll-down (&optional arg)
- "Scroll text of transient popup window down ARG lines.
+ "Scroll text of transient's menu window down ARG lines.
If ARG is nil scroll near full screen. This is a wrapper
around `scroll-down-command' (which see)."
(interactive "^P")
(scroll-down-command arg)))
(defun transient-backward-button (n)
- "Move to the previous button in the transient popup buffer.
+ "Move to the previous button in transient's menu buffer.
See `backward-button' for information about N."
(interactive "p")
(with-selected-window transient--window
(transient-show-summary (get-text-property (point) 'suffix)))))
(defun transient-forward-button (n)
- "Move to the next button in the transient popup buffer.
+ "Move to the next button in transient's menu buffer.
See `forward-button' for information about N."
(interactive "p")
(with-selected-window transient--window
beg 'face nil (line-end-position))))))
;;; Compatibility
-;;;; Popup Isearch
+;;;; Menu Isearch
(defvar-keymap transient--isearch-mode-map
:parent isearch-mode-map
2)
lisp-imenu-generic-expression :test #'equal)
+(defun transient--suspend-text-conversion-style ()
+ (static-if (boundp 'overriding-text-conversion-style) ; since Emacs 30.1
+ (when text-conversion-style
+ (letrec ((suspended overriding-text-conversion-style)
+ (fn (lambda ()
+ (setq overriding-text-conversion-style nil)
+ (remove-hook 'transient-exit-hook fn))))
+ (setq overriding-text-conversion-style suspended)
+ (add-hook 'transient-exit-hook fn)))))
+
(declare-function which-key-mode "ext:which-key" (&optional arg))
(defun transient--suspend-which-key-mode ()
(eval-when-compile
`((,(concat "("
(regexp-opt (list "transient-define-prefix"
+ "transient-define-group"
"transient-define-infix"
"transient-define-argument"
"transient-define-suffix")
(defun transient-lisp-variable--reader (prompt initial-input _history)
(read--expression prompt initial-input))
+;;;; `transient-cons-option'
+
+(defclass transient-cons-option (transient-option)
+ ((format :initform " %k %d: %v"))
+ "[Experimental] Class used for unencoded key-value pairs.")
+
+(cl-defmethod transient-infix-value ((obj transient-cons-option))
+ "Return ARGUMENT and VALUE as a cons-cell or nil if the latter is nil."
+ (and-let* ((value (oref obj value)))
+ (cons (oref obj argument) value)))
+
+(cl-defmethod transient-format-description ((obj transient-cons-option))
+ (or (oref obj description)
+ (let ((description (prin1-to-string (oref obj argument) t)))
+ (if (string-prefix-p ":" description)
+ (substring description 1)
+ description))))
+
+(cl-defmethod transient-format-value ((obj transient-cons-option))
+ (let ((value (oref obj value)))
+ (propertize (prin1-to-string value t) 'face
+ (if value 'transient-value 'transient-inactive-value))))
+
;;; _
(provide 'transient)
;; Local Variables: