Code examples for interactive explanations of transient.
This guide assumes you have minimal knowledge of Emacs, some programming
experience in elisp and non-lisp languages, and have at least seen screenshots
of magit
.
There are two ways:
- Open this file in Emacs and run examples as literate org.
- Install the package to run commands and read their source. Start with the
tsc-showcase
command.
If you open this file in Emacs, it will switch to Org mode and you can run
individual source blocks with org-babel-execute-src-blk
on the block.
If you install the package, you can read source for each example with the
normal. describe-function
command. All commands are under tsc-*
prefix.
Somewhat useful suffixes are under tsc-suffix-*
while less useful ones under
tsc--suffix-*
. They will come in handily when you are developing new
applications.
Installations for straight and elpaca:
;; using straight use-package with custom recipe
(use-package transient-showcase
:straight '(transient-showcase
:type git :host github :repo "positron-solutions/transient-showcase"))
;; using elpaca (recommended to add a hash for reproducibility)
(elpaca-use-package
(transient-showcase :host github
:repo "positron-solutions/transient-showcase")
:demand t)
Note While this file is also the README for this repository, it’s not intended to be used copy-paste. Many links will only open in Emacs. Some definitions are included by reference from Preludes
This file is produced with org-babel-tangle
. Package header and preludes
included in the no-web block below:
<<package-header>>
<<wave-prelude>>
<<predicates-prelude>>
<<show-level-prelude>>
<<levels-prelude>>
<<print-args-prelude>>
This is a basic transient, using an anonymous lambda interactive command as its only suffix.
(transient-define-prefix tsc-hello ()
"Prefix that is minimal and uses an anonymous command suffix."
[("s" "call suffix"
(lambda ()
(interactive)
(message "Called a suffix")))])
;; First, use M-x org-babel-execute-src-blk to cause `tsc-hello' to be defined
;; Second, M-x `eval-last-sexp' with your point at the end of the line below
;; (tsc-hello)
After executing the block above, you can execute-extended-command
(M-x)
and select tsc-hello
to show this transient. All transient prefixes are also
commands that show up in (M-x)
Note If the example above is hard to read, review some elisp syntax and typical forms.
- Terminology
- Declaring - Equivalent Forms
- Groups & Layouts
- Nesting & Flow Control
- Using & Managing States
- Controlling CLI’s
- Controlling Visibility
- Advanced
- Appendixes
- Further Reading
Transient means temporary. Transient gets its name from the temporary keymap and the popup UI for displaying that keymap. Emacs has a similar idea built-in with set-transient-map for a temporary high-precedence keymap.
The hello transient user input sequence is:
Prefix -> Suffix
- The prefix is the command you invoke first, such as
magit-dispatch
- A suffix is a command displayed in the transient UI, such as
magit-stage
(magit-dispatch) ; same as pressing 'h' in magit-status buffer
The keymap and UI display is frequently referred to as “a transient”. “Prefix” and “a transient” are almost the same thing. Invoking a prefix will show a transient. They are inseparable ideas.
Setting prefix arguments with =universal-argument= (=C-u=) is a distinct, separate behavior that is part of Emacs.
With prefix arguments, you “call” commands with extra arguments, like you would a function.
A transient prefix can set some states and its suffix can then use these states to tweak its behavior. The difference is that within the lifecycle of a transient UI, and coordinating with transient’s state persistence, you can create much more complex input to your commands. You can use commands to construct phrases for other commands.
To see a short example of prefix arguments being used within a transient prefix, see the scope example.
A prefix can also be bound as a suffix, enabling nested prefixes. A user input sequence with nested transients might look like:
Prefix -> Sub-Prefix -> Sub-Prefix -> Suffix
For example, in the magit-dispatch
transient (?
), l
for magit-log
is
a nested transient. b
for all branches
is the suffix command
magit-log-all-branches
.
See Flow Control for nested transient examples with both sub-prefixes and suffixes that do no exit.
Some suffixes need to hold state, toggling or storing an argument. Infixes are specialized suffixes to set and hold state. A user input sequence with infixes:
Prefix -> Infix -> Infix -> Suffix
See Infix examples to get a better idea.
- Prefixes display the pop-up UI and bind the keymap.
- Suffixes are commands bound within a prefix
- Infixes are a specialized suffix for storing and setting state
- A Suffix may be yet another Prefix, in which case the transient is nested
- The Shorthand form
- Keyword Arguments Style
- Macro Child Definition Style
- Overriding slots in the prefix definition
- Quoting Note for Vectors
You can declare the same behavior 3-4 ways
- Shorthand forms within
transient-define-prefix
macro allow shorthand binding of suffixes & commands or creation of infixes directly within the layout definition. - Macros for suffixes and infix definition streamline defining commands while also defining how they will behave in a layout.
- Keyword arguments
(:foo val1 :bar val2)
are interpreted by the macros and used to set slots (OOP attributes) on prefix, group, and suffix objects. Similar forms for declaring suffixes can be used to modify them when declaring a layout. Very specific control over layouts also uses these forms.;; slots & methods that can be set / overridden in children (describe-function transient-child)
- Custom classes using EIEIO (basically elisp OOP) can change methods deeper
in the implementation than you can reach with slots.
describe-function
is a quick way to look at the methods.;; slots & methods that can be set / overridden in suffixes (describe-function transient-suffix)
See the EIEIO Appendix for introduction to exploring EIEIO objects and classes.
Binding suffixes with the ("key" "description" suffix-or-command)
form
within a group is extremely common.
(transient-define-prefix tsc-wave ()
"Prefix that waves at the user"
[("w" "wave" tsc-suffix-wave)]) ; tsc-suffix-wave is a simple command from wave-prelude
;; (tsc-wave)
Note: Both commands and suffixes from transient-define-suffix
can be
used. It’s a good reason to use private--namespace
style names for suffix
actions since these commands don’t usually show up in (M-x) by default.
You can customize the slot value (OOP attribute) of the transient, groups,
and suffixes by adding extra :foo value
style pairs.
Not all behaviors have a shorthand form, so as you use more behaviors, you
will see more of the keyword argument style API. Here we use the
:transient
property, set to true, meaning the suffix won’t exit the
transient.
(transient-define-prefix tsc-wave-keyword-args ()
"Prefix that waves at the user persistently."
[("e" "wave eventually & stay" tsc--wave-eventually :transient t)
("s" "wave surely & leave" tsc--wave-surely :transient nil)])
;; (tsc-wave-keyword-args)
Launch the command, wave several times (note timestamp update) and then exit with (C-g).
The transient-define-suffix
macro can help if you need to bind a command in
multiple places and only override some properties for some prefixes. It
makes the prefix definition more compact at the expense of a more verbose
command.
(transient-define-suffix tsc-suffix-wave-macroed ()
"Prefix that waves with macro-defined suffix."
:transient t
:key "T"
:description "wave from macro definition"
(interactive)
(message "Waves from a macro definition at: %s" (current-time-string)))
;; Suffix definition creates a command
;; (tsc-suffix-wave-macroed)
;; Because that's where the suffix object is stored
;; (get 'tsc-suffix-wave-macroed 'transient--suffix)
;; tsc-suffix-wave-suffix defined above
(transient-define-prefix tsc-wave-macro-defined ()
"Prefix to wave using a macro-defined suffix."
[(tsc-suffix-wave-macroed)]) ; note, information moved from prefix to the suffix.
;; (tsc-wave-macro-defined)
Even if you define a property via one of the macros, you can still override
that property in the later prefix definition. The example below overrides
the :transient
, :description
, and :key
properties of the
tsc-suffix-wave
suffix defined above:
(defun tsc--wave-override ()
"Vanilla command used to override suffix's commands."
(interactive)
(message "This suffix was overridden. I am what remains."))
(transient-define-prefix tsc-wave-overridden ()
"Prefix that waves with overridden suffix behavior."
[(tsc-suffix-wave-macroed
:transient nil
:key "O"
:description "wave overridingly"
:command tsc--wave-override)]) ; we overrode what the suffix even does
;; (tsc-wave-overridden)
If you just list the key and symbol followed by properties, it is also a supported shorthand suffix form:
("wf" tsc-suffix-wave :description "wave furiously")
Inside the [ ...vectors... ]
in transient-define-prefix
, you don’t need
to quote symbols because in the vector, everything is a literal. When you
move a shorthand style :property symbol
out to the
transient-define-suffix
form, which is a list, you might need to quote the
symbol as :property 'symbol
.
To define a transient, you need at least one group. Groups are
vectors, delimited as [ ...group... ]
.
There is basic layout support and you can use it to collect or differentiate commands.
If you begin a group vector with a string, you get a group heading. Groups also support some properties. The group class also has a lot of information.
Very straightforward. Just make the first element in the vector a string or
add a :description
property, which can be a function.
In the prefix definition of suffixes, the second string is a description.
The :description
key is applied last and therefore wins in ambiguous
declarations.
(transient-define-prefix tsc-layout-descriptions ()
"Prefix with descriptions specified with slots."
["Let's Give This Transient a Title\n" ; yes the newline works
["Group One"
("wo" "wave once" tsc-suffix-wave)
("wa" "wave again" tsc-suffix-wave)]
["Group Two"
("ws" "wave some" tsc-suffix-wave)
("wb" "wave better" tsc-suffix-wave)]]
["Bad title" :description "Group of Groups"
["Group Three"
("k" "bad desc" tsc-suffix-wave :description "key-value wins")
("n" tsc-suffix-wave :description "no desc necessary")]
[:description "Key Only Def"
("wt" "wave too much" tsc-suffix-wave)
("we" "wave excessively" tsc-suffix-wave)]])
;; (tsc-layout-descriptions)
Note: The property list style for dynamic descriptions is the same for both
prefixes and suffixes. Add :description symbol-or-lambda-form
to the group
vector or suffix list.
(transient-define-prefix tsc-layout-dynamic-descriptions ()
"Prefix that generate descriptions dynamically when transient is shown."
;; group using function-name to generate description
[:description current-time-string
;; single suffix with dynamic description
("wa" tsc-suffix-wave
:description (lambda ()
(format "Wave at %s" (current-time-string))))]
;; group with anonymoous function generating description
[:description (lambda ()
(format "Group %s" (org-id-new)))
("wu" "wave uniquely" tsc-suffix-wave)])
;; (tsc-layout-dynamic-descriptions)
Note, the uuid is generated on every key input. Layout updates are fun. It does not also work when changing descriptions in the layout via hackery. 凸( ` ロ ´ )凸
The default behavior treats groups a little differently depending on how they are nested. For most simple groupings, this is sufficient control.
Use a vector for each row.
(transient-define-prefix tsc-layout-stacked ()
"Prefix with layout that stacks groups on top of each other."
["Top Group" ("wt" "wave top" tsc-suffix-wave)]
["Bottom Group" ("wb" "wave bottom" tsc-suffix-wave)])
;; (tsc-layout-stacked)
Use a vector of vectors for columns.
(transient-define-prefix tsc-layout-columns ()
"Prefix with side-by-side layout."
[["Left Group" ("wl" "wave left" tsc-suffix-wave)]
["Right Group" ("wr" "wave right" tsc-suffix-wave)]])
;; (tsc-layout-columns)
Vector on top of vector inside a vector.
(transient-define-prefix tsc-layout-stacked-columns ()
"Prefix with stacked columns layout."
["Top Group"
("wt" "wave top" tsc-suffix-wave)]
[["Left Group"
("wl" "wave left" tsc-suffix-wave)]
["Right Group"
("wr" "wave right" tsc-suffix-wave)]])
;; (tsc-layout-stacked-columns)
*Note: Groups can have groups or suffixes, but not both. You can’t mix suffixes alongside groups in the same vector. The resulting transient will error when invoked.*
Groups that are empty or only space have no effect. This situation can happen with layouts that update dynamically. See dynamic layouts.
(transient-define-prefix tsc-layout-spaced-out ()
"Prefix lots of spacing for users to space out at."
["" ; cannot add another empty string because it will mix suffixes with groups
["Left Group"
""
("wl" "wave left" tsc-suffix-wave)
("L" "wave lefter" tsc-suffix-wave)
""
("bl" "wave bottom-left" tsc-suffix-wave)
("z" "zone\n" zone)] ; the newline does pad
[[]] ; empty vector will do nothing
[""] ; vector with just empty line has no effect
;; empty group will be ignored
;; (useful for hiding in dynamic layouts)
["Empty Group\n"]
["Right Group"
""
("wr" "wave right" tsc-suffix-wave)
("R" "wave righter" tsc-suffix-wave)
""
("br" "wave bottom-right" tsc-suffix-wave)]])
;; (tsc-layout-spaced-out)
So, you put columns into rows that are in columns and stuff like that. This can be achieved with or without explicit column settings.
(transient-define-prefix tsc-layout-the-grid ()
"Prefix with groups in a grid-like arrangement."
[:description "The Grid\n" ; must use slot or macro is confused
["Left Column" ; note, no newline
("ltt" "left top top" tsc-suffix-wave)
("ltb" "left top bottom" tsc-suffix-wave)
""
("lbt" "left bottom top" tsc-suffix-wave)
("lbb" "left bottom bottom" tsc-suffix-wave)] ; note, no newline
["Right Column\n"
("rtt" "right top top" tsc-suffix-wave)
("rtb" "right top bottom" tsc-suffix-wave)
""
("rbt" "right bottom top" tsc-suffix-wave)
("rbb" "right bottom bottom\n" tsc-suffix-wave)]])
;; (tsc-layout-the-grid)
Note, only transient-columns
, not transient-column
can act as a group
of groups.
If you need to override the class that the transient-define-prefix
macro
would normally use.
(transient-define-prefix tsc-layout-explicit-classes ()
"Prefix with group class used to explicitly specify layout."
[:class transient-row "Row"
("l" "wave left" tsc-suffix-wave)
("r" "wave right" tsc-suffix-wave)]
[:class transient-column "Column"
("t" "wave top" tsc-suffix-wave)
("b" "wave bottom" tsc-suffix-wave)])
;; (tsc-layout-explicit-classes)
Many transients call other transients. This allows you to express similar behaviors as interactive commands that ask you for multiple arguments using the minibuffer.
Transient has more options for retaining some state across several transients, making it easier to compose commands and to retain intermediate states for rapidly achieving series of actions over similar inputs.
Sometimes you want to execute multiple commands without re-opening the transient. It’s the same idea as god mode or Evil repeat.
(transient-define-prefix tsc-stay-transient ()
"Prefix where some suffixes do not exit."
["Exit or Not?"
;; this suffix will not exit after calling sub-prefix
("we" "wave & exit" tsc-wave-overridden)
("ws" "wave & stay" tsc-wave :transient t)])
;; (tsc-stay-transient)
Note, if tsc-wave
was used in both exit & stay, the :transient
slot
would be clobbered and we would only get one behavior. Beware of re-using
the same object instances in the same layout. Move the :transient
slot
override between the two suffixes to see the change in behavior.
Nesting is putting transients inside other transients, creating user-input sequences like:
Prefix -> Sub-Prefix -> Suffix
This is the most simple way to create nesting.
(transient-define-prefix tsc--simple-child ()
["Simple Child"
("wc" "wave childishly" tsc-suffix-wave)])
(transient-define-prefix tsc-simple-parent ()
"Prefix that calls a child prefix."
["Simple Parent"
("w" "wave parentally" tsc-suffix-wave)
("b" "become child" tsc--simple-child)])
;; (tsc--simple-child)
;; (tsc-simple-parent)
Declaring a nested prefix that “returns” to its parent has a convenient shorthand form.
(transient-define-prefix tsc-simple-parent-with-return ()
"Prefix with a child prefix that returns."
["Parent With Return"
("w" "wave parentally" tsc-suffix-wave)
("b" "become child with return" tsc--simple-child :transient t)])
;; Child does not "return" when called independently
;; (tsc--simple-child)
;; (tsc-simple-parent-with-return)
If you call (transient-setup 'transient-command-symbol)
, you will activate
a replacement transient.
This form is useful if you want a command to perhaps load yet another transient in some situation. You may even just want to load the same transient with different context, such as passing in a new scope.
(transient-define-suffix tsc-suffix-setup-child ()
"A suffix that uses `transient-setup' to manually load another transient."
(interactive)
;; note that it's usually during the post-command side of calling the
;; command that the actual work to set up the transient will occur.
;; This is an implementation detail because it depends if we are calling
;; `transient-setup' while already transient or not.
(transient-setup 'tsc--simple-child))
(transient-define-prefix tsc-parent-with-setup-suffix ()
"Prefix with a suffix that calls `transient-setup'."
["Simple Parent"
("wp" "wave parentally" tsc-suffix-wave :transient t) ; remain transient
;; You may need to specify a different pre-command (the :transient) key
;; because we need to clean up this transient or create some conditions
;; to trigger the following transient correctly. This example will
;; work with `transient--do-replace' or no custom pre-command
("bc" "become child" tsc-suffix-setup-child :transient transient--do-replace)])
;; (tsc-parent-with-setup-suffix)
This example should also work with the transient--do-recurse
pre-command,
but the child transient does not return. There is a difference in the
behavior that should not depend on if the suffix is the prefix or just sets
up the prefix. Possible bug.
You can mix normal Emacs completion flows with transient UI’s.
See Interactive codes are listed in the Elisp manual.
Note, this also works when binding existing commands that recieve user input.
(transient-define-suffix tsc--suffix-interactive-string (user-input)
"An interactive suffix that obtains string input from the user."
(interactive "sPlease just tell me what you want!: ")
(message "I think you want: %s" user-input))
(transient-define-suffix tsc--suffix-interactive-buffer-name (buffer-name)
"An interactive suffix that obtains a buffer name from the user."
(interactive "b")
(message "You selected: %s" buffer-name))
(transient-define-prefix tsc-interactive-basic ()
"Prefix with interactive user input."
["Interactive Command Suffixes"
("s" "enter string" tsc--suffix-interactive-string)
("b" "select buffer" tsc--suffix-interactive-buffer-name)])
;; (tsc-interactive-basic)
Sometimes you can complete your work without asking the user for more input.
In the custom body for a prefix, if you decline to call transient-setup
,
then the command will just exit with no problems.
Below is a nested transient.
- The body form of the nested child can return early without loading a new transient
- The parent uses
transient--do-recurse
to make it’s child “return” to it - The “radiations” command in the child explicitly overrides this, using
transient--do-exit
so that it does not return to the parent
(defvar tsc--complex nil "Show complex menu or not.")
(transient-define-suffix tsc--toggle-complex ()
"Toggle `tsc--complex'."
:transient t
:description (lambda () (format "toggle complex: %s" tsc--complex))
(interactive)
(setf tsc--complex (not tsc--complex))
(message (propertize (concat "Complexity set to: "
(if tsc--complex "true" "false"))
'face 'success)))
(transient-define-prefix tsc-complex-messager ()
"Prefix that sends complex messages, unles `tsc--complex' is nil."
["Send Complex Messages"
("s" "snow people"
(lambda () (interactive)
(message (propertize "snow people! ☃" 'face 'success))))
("k" "kitty cats"
(lambda () (interactive)
(message (propertize "🐈 kitty cats! 🐈" 'face 'success))))
("r" "radiations"
(lambda () (interactive)
(message (propertize "Oh no! radiation! ☢" 'face 'success)))
;; radiation is dangerous!
:transient transient--do-exit)]
(interactive)
;; The command body either sets up the transient or simply returns
;; This is the "early return" we're talking about.
(if tsc--complex
(transient-setup 'tsc-complex-messager)
(message "Simple and boring!")))
(transient-define-prefix tsc-simple-messager ()
"Prefix that toggles child behavior!"
[["Send Message"
;; using `transient--do-recurse' causes suffixes in tsc-child to perform
;; `transient--do-return' so that we come back to this transient.
("m" "message" tsc-complex-messager :transient transient--do-recurse)]
["Toggle Complexity"
("t" tsc--toggle-complex)]])
;; (tsc-simple-messager)
;; does not "return" when called independently
;; (tsc-complex-messager)
The value in the :transient
slot affects what state the body of your
command will see and what will happen after your command, during the
post-command.
The :transient
slot holds a function called the “pre-command.” Before your
suffix body forms run, the pre-command is called and creates the conditions
that your suffix may use to, for example, prepare for reading variables that
were set on infixes. If the pre-command calls transient-export
then it
will add to history.
In transient-define-prefix
and transient-define-suffix
, the t
value is
actually translated to transient--do-call
or transient--do-recurse
depending on the situation.
These functions set up some states so that post-command can figure out if it needs to exit, save values, or enter another transient, and what else to do while entering that new transient.
The official long manual has some more detail. These examples should prepare you to visualize the forms used in those explanations.
Some of the trickiest bugs you can introduce will happen when using the following variables and functions at varying points in command lifecycles:
transient-current-command
transient--command
transient-current-prefix
transient--prefix
transient-args
During the pre-command and post-command, these can change. When you are
overriding the pre-command, you may discover things such as the result of
transient-args
changing. Calling transient-setup
may update things.
Even if you call transient-args
on on the specific transient, the results
change during the lifecycle and depending on the pre-command.
In particular it seems like layout predicates should use
transient--prefix
while suffix bodies should use
transient-current-prefix
.
Not all pre-commands are compatible with all situations and suffixes!
There’s definitely some edge cases that are unnecessarily complex for the
use case. Think of how life was before transient--do-recurse
.
- The Magic of Transient
- Infixes
- Scope
- Prefix Value & History
- History Keys
- Disabling Set / Save on a Suffix
- Setting or Saving Every Time a Suffix is Used
- Lisp Variables
There are several ways to create state. The flow control examples in the previous section mainly covered how to get from one command to the other. This section covers how to save values and then read them later, sometimes from a completely different transient. Coupled with custom infix types, you can create some seriously rich user expression.
To spark your imagination, here’s a non-exhaustive list of how to get data into your commands:
- Interactive forms
- Prefix arguments (
C-u
universal argument) - Setting the scope in
transient-setup
- Obtaining a scope in a custom
transient-init-scope
method - Default values in prefix definition
- Saved values of infixes
- Saved values in other infixes / prefixs with shared
history-key
- User-set infix values from the current or parent prefix
- Ad-hoc values in regular
defvar
anddefcustom
etc - Reading values from another, perhaps distant prefix
- Arguments passed into interactive commands to call them as normal elisp functions
Using all of these mechanisms, you can enable users to rapidly construct complex command sentences, sentences with phrases. You can basically make a user interface as expressive as elisp.
A user input sequence like this:
Prefix -> Interactive -> Sub-Prefix -> Infix -> Suffix -> Suffix -> ...
Is basically the same as doing this in elisp:
(let ((input (Sub-Prefix (Prefix (Interactive))))
(infix (Infix)))
(suffix input infix)
(suffix input infix))
With history, you can remember lots of these states. This allows the user to quickly fire off lots of mostly completed partial expressions. They are scoped, so you can keep state over different contexts.
This is what is meant by “creating user interfaces as expressive as elisp.”
Because interactive forms and transients are both still just consuming linear user input, they ultimately have the same capabilities, but if you think in terms of partially constructed elisp expressions, you can do more than if the user has to enter in contextless commands over and over or write more commands while managing their own state in ad-hoc fashion.
Transient’s UI also provides greater awareness to the user of the current state. This makes it easier for the user to achieve the greater complexity that is intended, without remembering the command language you are designing for your application.
Functions need arguments. Infixes are specialized suffixes with behavior defaults that make sense for setting and storing values for consumption in suffixes. It’s like passing arguments into the suffix. They also have support for persisting state across invocations and Emacs sessions.
Infix classes built-in all descend from transient-infix
and can be seen
clearly in the eieio-browse
. View their slots and documentaiton with
(describe-class transient-infix)
etc. Here you can see what most infixes
look like and how they behave.
;; infix defined with a macro
(transient-define-argument tsc--exclusive-switches ()
"This is a specialized infix for only selecting one of several values."
:class 'transient-switches
:argument-format "--%s-snowcone"
:argument-regexp "\\(--\\(grape\\|orange\\|cherry\\|lime\\)-snowcone\\)"
:choices '("grape" "orange" "cherry" "lime"))
(transient-define-prefix tsc-basic-infixes ()
"Prefix that just shows off many typical infix types."
["Infixes"
;; from macro
("-e" "exclusive switches" tsc--exclusive-switches)
;; shorthand definitions
("-b" "switch with shortarg" ("-w" "--switch-short")) ; with :short-arg != :key
("-s" "switch" "--switch")
( "n" "no dash switch" "still works")
("-a" "argument" "--argument=" :prompt "Let's argue because: ")
;; a bit of inline EIEIO in our shorthand
("-n" "never empty" "--non-null=" :always-read t :allow-empty nil
:init-value (lambda (obj) (oset obj value "better-than-nothing")))
("-c" "choices" "--choice=" :choices (foo bar baz))]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-basic-infixes)
Reminder in the section on pre-commands the discussion about the
:transient
mentions that the values available in a suffix body depend on
whe ther the pre-command called transient--export
before evaluating the
suffix body.
There are two basic ways to read infixes:
(transient-args transient-current-command)
and parse manually(transient-arg-value "--argument-" (transient-args transient-current-command)
(transient-suffixes transient-current-command)
and retrieve your fully hydrated suffix
In my opinion the API should make it easer to get raw values from suffixes,
but this is also a matter of custom infixes needing to serialize values
correctly so that transient-arg-value
will “just work”.
When you call a function with an argument, you want to know in the body of
your function what that argument was. This is the scope. The prefix is
initialized with the :scope
either in it’s own body or a similar form.
Suffixes can then read back that scope in their body. The suffix object is
given the scope and can use it to alter its own display or behavior. The
layout also can interpret the scope while it is initializing.
WARNING When writing predicates against the scope, you will need to
determine whether transient--prefix
or transient-current-prefix
is
correct when writing prefix-generic suffixes. It is very subtle if you
accidentally choose the wrong one and the parent has a nil scope while the
child has an entirely different scope. These variables change throughout the
lifecycle! Use edebug you must!
(transient-define-suffix tsc--read-prefix-scope ()
"Read the scope of the prefix."
:transient 'transient--do-call
(interactive)
(let ((scope (oref transient-current-prefix scope)))
(message "scope: %s" scope)))
(transient-define-suffix tsc--double-scope-re-enter ()
"Re-enter the current prefix with double the scope."
;; :transient 'transient--do-replace ; builds up the stack
:transient 'transient--do-exit
(interactive)
(let ((scope (oref transient-current-prefix scope)))
(if (numberp scope)
(transient-setup transient-current-command nil nil :scope (* scope 2))
(message (propertize (format "scope was non-numeric! %s" scope) 'face 'warning))
(transient-setup transient-current-command))))
(transient-define-suffix tsc--update-scope-with-prefix-re-enter (new-scope)
"Re-enter the prefix with double the scope."
;; :transient 'transient--do-replace ; builds up the stack
:transient 'transient--do-exit ; do not build up the stack
(interactive "P")
(message "universal arg: %s" new-scope)
(transient-setup transient-current-command nil nil :scope new-scope))
(transient-define-prefix tsc-scope (scope)
"Prefix demonstrating use of scope."
;; note! this is a location where we definitely had to use
;; `transient--prefix' or get the transient object from the tsc-scope symbol.
;; `transient-current-prefix' is not correct here!
[:description (lambda () (format "Scope: %s" (oref transient--prefix scope)))
[("r" "read scope" tsc--read-prefix-scope)
("d" "double scope" tsc--double-scope-re-enter)
("o" "update scope (use prefix argument)" tsc--update-scope-with-prefix-re-enter)]]
(interactive "P")
(transient-setup 'tsc-scope nil nil :scope scope))
;; Setting an interactive argument for `eval-last-sexp' is a little different
;; (let ((current-prefix-arg 4)) (call-interactively 'tsc-scope))
;; (tsc-scope)
;; Then press "C-u 4 o" to update the scope
;; Then d to double
;; Then r to read
;; ... and so on
;; C-g to exit
Key binding sequences, such as “wa” instead of single-key prefix bindings
will unset the prefix argument (the old-school Emacs C-u
prefix argument,
not the prefix’s scope or other explicit arguments)
Possibly a bug in transient.
Briefly, there are three locations for state you need to be aware of for this section:
- Each transient’s prefix object has a
:value
that is updated bytransient-set
andtransient-save
- The values obtained from
transient-args
are usually quite ephemeral and don’t even persist beyond the body of form of the suffixes you usually read them in transient-values
contains saved values that are used to rehydrate the prefix:value
slot when the prefix is createdtransient-history
is used to make it faster for the user to flip through previous states (which can have independent histories for infixes and prefixes). These are never used unless callingtransient-history-prev
andtransient-history-next
.
We can get this as a list of strings for any prefix by calling
transient-args
on transient-current-command
in the suffix’s interactive
form. If you know the command you want the value of, you can use it’s symbol
instead of transient-current-command
.
This is related to history keys. If you set the arguments and then save them
using (C-x s
) for the command transient-save
, not only will the transient
be updated with the new value, but if you call the child independently, it can
still read the value from the suffix.
(transient-define-suffix tsc-suffix-eat-snowcone (args)
"Eat the snowcone!
This command can be called from it's parent, `tsc-snowcone-eater' or independently."
:transient t
;; you can use the interactive form of a command to obtain a default value
;; from the user etc if the one obtained from the parent is invalid.
(interactive (list (transient-args 'tsc-snowcone-eater)))
;; `transient-arg-value' can (with varying success) pick out individual
;; values from the results of `transient-args'.
(let ((topping (transient-arg-value "--topping=" args))
(flavor (transient-arg-value "--flavor=" args)))
(message "I ate a %s flavored snowcone with %s on top!" flavor topping)))
(transient-define-prefix tsc-snowcone-eater ()
"Prefix demonstrating set & save infix persistence."
;; This prefix has a default value that tsc-suffix-eat-snowcone can see
;; even before the prefix has been called.
:value '("--topping=fruit" "--flavor=cherry")
;; always-read is used below so that you don't save nil values to history
["Arguments"
("-t" "topping" "--topping="
:choices ("ice cream" "fruit" "whipped cream" "mochi")
:always-read t)
("-f" "flavor" "--flavor="
:choices ("grape" "orange" "cherry" "lime")
:always-read t)]
;; Definitely check out the =C-x= menu
["C-x Menu Behaviors"
("S" "save snowcone settings"
(lambda () (interactive) (message "saved!") (transient-save)) :transient t)
("R" "reset snowcone settings"
(lambda () (interactive) (message "reset!") (transient-reset)) :transient t)]
["Actions"
("m" "message arguments" tsc-suffix-print-args)
("e" "eat snowcone" tsc-suffix-eat-snowcone)])
;; First call will use the transient's default value
;; M-x tsc-suffix-eat-snowcone or `eval-last-sexp' below
;; (call-interactively 'tsc-suffix-eat-snowcone)
;; (tsc-snowcone-eater)
;; Eat some snowcones with different flavors
;; ...
;; ...
;; ...
;; Now save the value and exit the transient.
;; When you call the suffix independently, it can still read the saved values!
;; M-x tsc-suffix-eat-snowcone or `eval-last-sexp' below
;; (call-interactively 'tsc-suffix-eat-snowcone)
It’s worth bringing up the =transient-show-common-commands= variable. You may
want to set this when working on the history support for your transients.
Otherwise, just remember the (C-x
) menu inside transients.
History lets you set infixes using prior values. It’s per-prefix,
per-suffix usually. Using previous examples like tsc-snowcone-eater
, you
can flip through history using:
C-x p
fortransient-history-prev
C-x n
fortransient-history-next
These bindings are revealed when transient-show-common-commands
is t
or
when you hit the C-x
prefix.
However, what if you don’t want a unique history for some infixes or even prefixes?
Note As a more advanced example, using EIEIO and dynamic layout techniques
to modify the slot of :history-key
, you can also make unique histories for
the same prefix/infix by setting that slot value depending on the context you
want unique histories for.
The following example can demonstrate the behavior with some user effort:
(transient-define-prefix tsc-ping ()
"Prefix demonstrating history sharing."
:history-key 'non-unique-name
["Ping"
("-g" "game" "--game=")
("p" "ping the pong" tsc-pong)
("a" "print args" tsc-suffix-print-args :transient nil)])
(transient-define-prefix tsc-pong ()
"Prefix demonstrating history sharing."
:history-key 'non-unique-name
["Pong"
("-g" "game" "--game=")
("p" "pong the ping" tsc-ping)
("a" "print args" tsc-suffix-print-args :transient nil)])
;; (tsc-ping)
;; Okay here's where it gets weird
;; 1. Set the value of game to something and remember it
;; 2. Press a to print the args
;; 3. Re-open tsc-ping.
;; 4. C-x p to load the previous history, see the old value?
;; 5. p to switch to the tsc-pong transient
;; 6. C-x p to load the previous history, see the old value from tsc-ping???
;; 7. Note that tsc-pong uses the same history as tsc-ping!
Set values show up in the prefix’s value
slot.
(oref (plist-get (symbol-plist 'tsc-ping) 'transient--prefix) value)
The prefix value will get the last value that was set using
transient-set
.
However, the prefix value shown in transient-values
is only updated when
calling transient-save
.
Saved values show up in transient-values
. If you save tsc-ping
, you can
see the saved value here:
(assoc 'tsc-ping transient-values)
These two values may be independent. They are written at the same time
when calling transient-save
. During prefix initializaton, the :value
is
written from transient-values
.
Play with the tsc-snowcone-eater
and tsc-ping
and tsc-pong
in the C-x
menu while also looking at what gets stored in transient-values
,
transient-history
and the prefix’s slots.
When you re-evaluate the prefix or reload Emacs, you will see the result of
initialization from transient-values
.
To disable saving and setting values, causing a prefix to always end up using
the default value, set the :unsavable
slot to t
.
(transient-define-prefix tsc-goldfish ()
"A prefix that cannot remember anything."
["Goldfish"
("-r" "rememeber" "--i-remember="
:unsavable t ; infix isn't saved
:always-read t ; infix always asks for new value
;; overriding the method to provide a starting value
:init-value (lambda (obj) (oset obj value "nothing")))
("a" "print args" tsc-suffix-print-args :transient nil)])
;; (tsc-goldfish)
Try to update remember
and then set and save it in the C-x
menu. Reload
it. It will never pay attention to history or setting & saving the transient
value.
(transient-define-suffix tsc-suffix-remember-and-wave ()
"Wave, and force the prefix to set it's saveable infix values."
(interactive)
;; (transient-reset) ; forget
(transient-set) ; save for this session
;; If you combine reset with set, you get a reset for future sessions only.
;; (transient-save) ; save for this and future sessions
;; (transient-reset-value some-other-prefix-object)
(message "Waves at user at: %s. You will never be forgotten." (current-time-string)))
(transient-define-prefix tsc-elephant ()
"A prefix that always remembers its infixes."
["Elephant"
("-r" "rememeber" "--i-remember="
:always-read t)
("w" "remember and wave" tsc-suffix-remember-and-wave)
("a" "print args (skips remembering)" tsc-suffix-print-args :transient nil)])
;; (tsc-elephant)
There needs to be a slot that causes infixes to always be set on export.
This would cover cases where the most frequent user input changes just
rapidly enough that both setting every time and saving are equally
inconvenient. Using transient-set
is kind of brute-ish.
Every transient prefix has a value. It’s a list. You can set it to create defaults for switches and arguments.
(transient-define-prefix tsc-default-values ()
"A prefix with a default value."
:value '("--toggle" "--value=5")
["Arguments"
("t" "toggle" "--toggle")
("v" "value" "--value=" :prompt "an integer: ")]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-default-values)
Note, after setting or saving a value on this transient using the C-x
menu, the next time the transient is set up, it will have a different
value. If you want the default to return, use transient-reset
in your
suffix.
Readers are the mechanism to provide completions and to enforce input validity of infixes.
(transient-define-prefix tsc-enforcing-inputs ()
"A prefix with enforced input type."
["Arguments"
("v" "value" "--value=" :prompt "an integer: " :reader transient-read-number-N+)]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-enforcing-inputs)
Setting the reader can be used to enforce rules of valid input. See
Advanced/Custom Infix Types for an example of writing a custom reader that
validates input and assigning that reader via the class method instead of the
:reader
slot.
Lisp variables are currently at an experimental support level. They way they
work is to report and set the value of a lisp symbol variable. Because they
aren’t necessarilly intended to be printed as crude CLI arguments, they DO
NOT appear in (transient-args 'prefix)
but this is fine because you can
just use the variable.
Customizing this class can be useful when working with objects and functions that exist entirely in elisp.
(defvar tsc--position '(0 0) "A transient prefix location.")
(transient-define-infix tsc--pos-infix ()
"A location, key, or command symbol."
:class 'transient-lisp-variable
:transient t
:prompt "An expression such as (0 0), \"p\", nil, 'tsc--msg-pos: "
:variable 'tsc--position)
(transient-define-suffix tsc--msg-pos ()
"Message the element at location."
:transient 'transient--do-call
(interactive)
;; lisp variables are not sent in the usual (transient-args) list.
;; Just read `tsc--position' directly.
(let ((suffix (transient-get-suffix transient-current-command tsc--position)))
(message "%s" (oref suffix description))))
(transient-define-prefix tsc-lisp-variable ()
"A prefix that updates and uses a lisp variable."
["Location Printing"
[("p" "position" tsc--pos-infix)]
[("m" "message" tsc--msg-pos)]])
;; (tsc-lisp-variable)
This section covers more usages of infixes, focused on creating better argument strings for CLI tools.
The section on flow control & managing state has more information about controlling elisp applications.
Note: these forms are generic for different prefixes, allowing you to mix and match suffixes within prefixes.
The shorthand forms in transient-define-prefix
are heavily influenced by
the CLI style switches and arguments that transient was built to
control. Most shorthand forms look like so:
("key" "description" "argument")
The macro will select the infix’s exact class depending on how you write
:argument
. If you write something ending in =
such as --value=
then
you get :class transient-option
but if not, the default is a :class
transient-switch
Use =(describe-function transient-option)= and =(describe-function transient-option)= to see a full document of their slots and methods.
If you need an argument with a space instead of the equal sign, use a space
and force the infix to be an argument by setting :class transient-option
.
(transient-define-prefix tsc-switches-and-arguments (arg)
"A prefix with switch and argument examples."
[["Arguments"
("-s" "switch" "--switch")
("-a" "argument" "--argument=")
("t" "toggle" "--toggle")
("v" "value" "--value=")]
["More Arguments"
("-f" "argument with forced class" "--forced-class " :class transient-option)
("I" "argument with inline" ("-i" "--inline-shortarg="))
("S" "inline shortarg switch" ("-n" "--inline-shortarg-switch"))]]
["Commands"
("w" "wave some" tsc-wave)
("s" "show arguments" tsc-suffix-print-args)]) ; use to analyze the switch values
;; (tsc-switches-and-arguments)
If you need to fine-tune a switch (boolean infix), use
transient-define-infix
. Likewise, use transient-define-argument
for
fine-tuning an argument. The class definitions can be used as a reference
while the manual provides more explanation.
(transient-define-infix tsc--random-init-infix ()
"Switch on and off."
:argument "--switch"
:shortarg "-s" ; will be used for :key when key is not set
:description "switch"
:init-value (lambda (obj)
(oset obj value
(eq 0 (random 2))))) ; write t with 50% probability
(transient-define-prefix tsc-maybe-on ()
"A prefix with a randomly intializing switch."
["Arguments"
(tsc--random-init-infix)]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-maybe-on)
;; (tsc-maybe-on)
;; ...
;; Run the command a few times to see the random initialization of `tsc--random-init-infix'
;; It will only take more than ten tries for one in a thousand users. Good luck.
Choices can be set for an argument. The property API and
transient-define-argument
are equivalent for configuring choices. You can
either hardcode or generate choices.
(transient-define-argument tsc--animals-argument ()
"Animal picker."
:argument "--animals="
; :multi-value t ; multi-value can be set to --animals=fox,otter,kitten etc
:class 'transient-option
:choices '("fox" "kitten" "peregrine" "otter"))
(transient-define-prefix tsc-animal-choices ()
"Prefix demonstrating selecting animals from choices."
["Arguments"
("-a" "--animals=" tsc--animals-argument)]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-animal-choices)
Choices can also be defined in a shorthand form. Use :class
'transient-option
if you need to force a different class to be used.
(transient-define-prefix tsc-animal-choices-shorthand ()
"Prefix demonstrating the shorthand style of defining choices."
["Arguments"
("-a" "Animal" "--animal=" :choices ("fox" "kitten" "peregrine" "otter"))]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-animal-choices-shorthand)
An argument with :class transient-switches
may be used if a set of
switches is exclusive. The key will likely not match the short argument.
Regex is used to tell the interface that you are entering one of the
choices. The selected choice will be inserted into :argument-format
. The
:argument-regexp
must be able to match any of the valid options.
The UX on mutually exclusive switches is a bit of a pain to discover. You must repeatedly press =:key= in order to cycle through the options.
(transient-define-argument tsc--snowcone-flavor ()
:description "Flavor of snowcone."
:class 'transient-switches
:key "f"
:argument-format "--%s-snowcone"
:argument-regexp "\\(--\\(grape\\|orange\\|cherry\\|lime\\)-snowcone\\)"
:choices '("grape" "orange" "cherry" "lime"))
(transient-define-prefix tsc-exclusive-switches ()
"Prefix demonstrating exclusive switches."
:value '("--orange-snowcone")
["Arguments"
(tsc--snowcone-flavor)]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-exclusive-switches)
If you need to prevent arguments in a group from being set simultaneously,
you can set the prefix property :incompatible
and a list of the long-style
argument.
Use a list of lists, where each sublist is the long argument style. Match
the string completely, including use of =
in both arguments and switches.
(transient-define-prefix tsc-incompatible ()
"Prefix demonstrating incompatible switches."
;; update your transient version if you experience #129 / #155
:incompatible '(("--switch" "--value=")
("--switch" "--toggle" "--flip")
("--argument=" "--value=" "--special-arg="))
["Arguments"
("-s" "switch" "--switch")
("-t" "toggle" "--toggle")
("-f" "flip" "--flip")
("-a" "argument" "--argument=")
("v" "value" "--value=")
("C-a" "special arg" "--special-arg=")]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-incompatible)
This section is incomplete. Maybe Magit contains better answers.
Sometimes the :shortarg
in a CLI doesn’t exactly match the :key:
and
:argument
, so it can be specified manually.
The :shortarg
concept could be used to help use man-pages or only for
transient-detect-key-conflicts but it’s not clear what behavior it changes.
Shortarg cannot be used for exclusion excluding other options (prefix
:incompatible
) or setting default values (prefix :value
).
See transient-infix-read
for actual code. This method uses the prefix’s
history and then delecates to completing-read
or
completing-read-multiple
. The :choices
key coresponds to the
COLLECTION
argument passed to completing reads.
Note, using a function for completions can appear to require a daunting amount of behavior if you read the manul <a href=”info:elisp#Programmed Completion”>section on programmed completions. If you however just return a list of options, even when FLAG is not t, everything seems just fine.
(defun tsc--animal-choices (_complete-me _predicate flag)
"Programmed completion for animal choice.
_COMPLETE-ME: whatever the user has typed so far
_PREDICATE: function you should use to filter candidates (only nil seen so far)
FLAG: request for metadata (which can be disrespected)"
;; if you want to respect metadata requests, here's what the form might
;; look like, but no behavior was observed.
(if (eq flag 'metadata)
'(metadata . '((annotation-function . (lambda (c) "an annotation"))))
;; when not handling a metadata request from completions, use some
;; logic to generate the choices, possibly based on input or some time
;; / context sensitive process. FLAG will be `t' when these are reqeusted.
(if (eq 0 (random 2))
'("fox" "kitten" "otter")
'("ant" "peregrine" "zebra"))))
(transient-define-prefix tsc-choices-with-completions ()
"Prefix with completions for choices."
["Arguments"
("-a" "Animal" "--animal="
:always-read t ; don't allow unsetting, just read a new value
:choices tsc--animal-choices)]
["Show Args"
("s" "show arguments" tsc-suffix-print-args)])
;; (tsc-choices-with-completions)
Switches and arguments that can be used multiple times are supported. Example needs to be written. This is useful for CLI wrapping or perhaps situations where a command accepts multiple levels of the same setting.
If you want to call a command line application using the arguments, you might need to do a bit of work processing the arguments. The following example uses cowsay.
- Cowsay doesn’t actually have a
message=
argument, So we end up stripping it from the arguments and re-assembling somethingcall-process
can use. - Cowsay supports more options, but for the sake of keeping this example small (and to refocus effort on transient itself), the set of all CLI options are not fully supported.
There’s some errata about this example:
- The predicates don’t update the transient.
(transient--redisplay)
doesn’t do the trick. We could usetransient--do-replace
andtransient-setup
, but that would lose existing state - The predicate needs to be exists & not empty (but doesn’t matter yet)
(defun tsc--quit-cowsay ()
"Kill the cowsay buffer and exit."
(interactive)
(kill-buffer "*cowsay*"))
(defun tsc--cowsay-buffer-exists-p ()
"Visibility predicate."
(not (equal (get-buffer "*cowsay*") nil)))
(transient-define-suffix tsc--cowsay-clear-buffer (&optional buffer)
"Delete the *cowsay* buffer. Optional BUFFER name."
:transient 'transient--do-call
:if 'tsc--cowsay-buffer-exists-p
(interactive) ; todo look at "b" interactive code
(save-excursion
(let ((buffer (or buffer "*cowsay*")))
(set-buffer buffer)
(delete-region 1 (+ 1 (buffer-size))))))
(transient-define-suffix tsc--cowsay (&optional args)
"Run cowsay."
(interactive (list (transient-args transient-current-command)))
(let* ((buffer "*cowsay*")
;; TODO ugly
(cowmsg (if args (transient-arg-value "--message=" args) nil))
(cowmsg (if cowmsg (list cowmsg) nil))
(args (if args
(seq-filter
(lambda (s) (not (string-prefix-p "--message=" s))) args)
nil))
(args (if args
(if cowmsg
(append args cowmsg)
args)
cowmsg)))
(when (tsc--cowsay-buffer-exists-p)
(tsc--cowsay-clear-buffer))
(apply #'call-process "cowsay" nil buffer nil args)
(switch-to-buffer buffer)))
(transient-define-prefix tsc-cowsay ()
"Say things with animals!"
; only one kind of eyes is meaningful at a time
:incompatible '(("-b" "-g" "-p" "-s" "-t" "-w" "-y"))
["Message"
("m" "message" "--message=" :always-read t)] ; always-read, so clear by entering empty string
[["Built-in Eyes"
("b" "borg" "-b")
("g" "greedy" "-g")
("p" "paranoid" "-p")
("s" "stoned" "-s")
("t" "tired" "-t")
("w" "wired" "-w")
("y" "youthful" "-y")]
["Actions"
("c" "cowsay" tsc--cowsay :transient transient--do-call)
""
("d" "delete buffer" tsc--cowsay-clear-buffer)
("q" "quit" tsc--quit-cowsay)]])
;; (tsc-cowsay)
Clean up cowsay example. Check for binary before attempting to run it.
At times, you need a prefix to show or hide certain options depending on the context.
Simple predicates at the group or element level exist to hide parts of the transient when they wouldn’t be useful at all in the situation.
(defvar tsc-busy nil "Are we busy?")
(defun tsc--busy-p () "Are we busy?" tsc-busy)
(transient-define-suffix tsc--toggle-busy ()
"Toggle busy."
(interactive)
(setf tsc-busy (not tsc-busy))
(message (propertize (format "busy: %s" tsc-busy)
'face 'success)))
Open the following example in buffers with different modes (or change modes manually) to see the different effects of the mode predicates.
(transient-define-prefix tsc-visibility-predicates ()
"Prefix with visibility predicates.
Try opening this prefix in buffers with modes deriving from different
abstract major modes."
["Empty Groups Not Displayed"
;; in org mode for example, this group doesn't appear.
("we" "wave elisp" tsc-suffix-wave :if-mode emacs-lisp-mode)
("wc" "wave in C" tsc-suffix-wave :if-mode cc-mode)]
["Lists of Modes"
("wm" "wave multiply" tsc-suffix-wave :if-mode (dired-mode gnus-mode))]
[["Function Predicates"
;; note, after toggling, the transient needs to be re-displayed for the
;; predicate to take effect
("b" "toggle busy" tsc--toggle-busy)
("bw" "wave busily" tsc-suffix-wave :if tsc--busy-p)]
["Programming Actions"
:if-derived prog-mode
("pw" "wave programishly" tsc-suffix-wave)
("pe" "wave in elisp" tsc-suffix-wave :if emacs-lisp-mode)]
["Special Mode Actions"
:if-derived special-mode
("sw" "wave specially" tsc-suffix-wave)
("sd" "wave dired" tsc-suffix-wave :if-mode dired-mode)]
["Text Mode Actions"
:if-derived text-mode
("tw" "wave textually" tsc-suffix-wave)
("to" "wave org-modeishly" tsc-suffix-wave :if-mode org-mode)]])
;; (tsc-visibility-predicates)
“Greyed out” suffixes. Inapt is better if an option is temporarily unavailable due to a state that varies with each invocation of the transient.
Inapt predicates work on suffixes, but not on groups (which would have to modify every child).
Note, like visibility predicates, inapt-*
predicates do not take effect
until the transient has it’s layout fully redone. Therefore this example
uses a child transient and updates the scope.
(defun tsc--child-scope-p ()
"Return the scope of the current transient.
When this is called in layouts, it's the transient being layed out"
(let ((scope (oref transient--prefix scope)))
(message "The scope is: %s" scope)
scope))
;; the wave suffixes were :transient t as defined, so we need to manually
;; override them to the `transient--do-return' value for :transient slot so
;; that they return back to the parent.
(transient-define-prefix tsc--inapt-children ()
"Prefix with children using inapt predicates."
["Inapt Predicates Child"
("s" "switched" tsc--wave-surely
:transient transient--do-return
:if tsc--child-scope-p)
("u" "unswitched" tsc--wave-normally
:transient transient--do-return
:if-not tsc--child-scope-p)]
;; in the body, we read the value of the parent and set our scope to
;; non-nil if the switch is set
(interactive)
(let ((scope (transient-arg-value "--switch" (transient-args 'tsc-inapt-parent))))
(message "scope: %s" scope)
(message "type: %s" (type-of scope))
(transient-setup 'tsc--inapt-children nil nil :scope (if scope t nil))))
(transient-define-prefix tsc-inapt-parent ()
"Prefix that configures child with inapt predicates."
[("-s" "switch" "--switch")
("a" "show arguments" tsc-suffix-print-args)
("c" "launch child prefix" tsc--inapt-children :transient transient--do-recurse)])
;; (tsc-inapt-parent)
There is not a single mention of inapt even though it’s fully implemented and works.
Levels are another way to control visibility.
- As a developer, you set levels to optionally expose or hide children in a prefix.
- As a user, you change the prefix’s level and the levels of suffixes to customize what’s visible in the transient.
Lower levels are more visible. Setting the level higher reveals more suffixes. 1-7 are valid levels.
The user can adjust levels within a transient prefix by using (C-x l) for
transient-set-level
. The default active level is 4, stored in
transient-default-level
. The default level for children is 1, stored in
transient--default-child-level
.
Per-suffix and per-group, the user can set the level at which the child will be visible. Each prefix has an active level, remembered per prefix. If the child level is less-than-or-equal to the child level, the child is visible.
A hidden group will hide a suffix even if that suffix is at a low enough level. Issue #153 has some addional information about behavior that might get cleaned up.
Adding default levels for children is as simple as adding integers at the beginning of each list or vector. If some commands are not likely to be used, instead of making the hard choice to include them or not, you can provide them, but tell the user in your README to set higher levels.
(transient-define-prefix tsc-levels-and-visibility ()
"Prefix with visibility levels for hiding rarely used commands."
[["Setting the Current Level"
;; this binding is normally not displayed. The value of
;; `transient-show-common-commands' controls this by default.
("C-x l" "set level" transient-set-level)
("s" "show level" tsc-suffix-show-level)]
[2 "Per Group" ; 1 is the default default-child-level
("ws" "wave surely" tsc--wave-surely) ; 1 is the default default-child-level
(3"wn" "wave normally" tsc--wave-normally)
(5"wb" "wave non-essentially" tsc--wave-non-essentially)]
[3 "Per Group Somewhat Useful"
("wd" "wave definitely" tsc--wave-definitely)]
[6 "Groups hide visible children"
(1 "wh" "wave hidden" tsc--wave-hidden)]
[5 "Per Group Rarely Useful"
("we" "wave eventually" tsc--wave-eventually)]])
;; (tsc-levels-and-visibility)
Press (C-x l) to open the levels UI for the user. Press (C-x l) again to change the active level. Press a key such as “we” to change the level for a child. After you cancel level editing with (C-g), you will see that children have either become visible or invisible depending on the changes you made.
*While a child may be visible according to its own level, if it’s hidden within the group, the user’s level-setting UI for the prefix will contradict what’s actually visible. The UI does not allow setting group levels.*
The previous sections are designed to go breadth-first so that you can get core ideas first. The following examples expand on combinations of several ideas or subclassing & customizing rarely used slots.
Some of these examples are approaching the complexity of just reading magit source.
While you can cover many cases using predicates, layouts, and visibility, sometimes you really do want to generate a list of commands.
Note, beware that you could be creating a lot of suffix objects if the forms you use generate unique symbols. These will pollute command completions over time, so probably don’t do that.
This is a group method that can be overridden in order to modify or eliminate some children from display. If you need a central place for children to coordinate some behavior, this may work for you.
(transient-define-prefix tsc-generated-child ()
"Prefix that uses `setup-children' to generate single child."
["Replace this child"
;; Let's override the group's method
:setup-children
(lambda (_) ; we don't care about the stupid suffix
;; remember to return a list
(list (transient-parse-suffix
transient--prefix
'("r" "replacement" (lambda ()
(interactive)
(message "okay!"))))))
("s" "haha stupid suffix" (lambda ()
(interactive)
(message "You should replace me!")))])
;; (tsc-generated-child)
transient--parse-child
takes the same configuration format as
transient-define-prefix
. You can see the layout format in the layout
hacking appendix. transient--prarse-group
works almost exactly the
same, just for groups.
The same thing, but parsing an entire group spec:
(transient-define-prefix tsc-generated-group ()
"Prefix that uses `setup-children' to generate a group."
["Replace this child"
;; Let's override the group's method
:setup-children
(lambda (_) ; we don't care about the stupid suffix
;; the result of parsing here will be a group
(transient-parse-suffixes
transient--prefix
["Group Name" ("r" "replacement" (lambda ()
(interactive)
(message "okay!")))]))
("s" "haha stupid suffix" (lambda ()
(interactive)
(message "You should replace me!")))])
;; (tsc-generated-group)
If you need to define a dynamic group class, override
transient-setup-children
. It will work almost entirely the same as the
examples above. Set your group class in the prefix using the :class
key.
Note you don’t need to be inside of a layout body to hack around with dynamic layouts. Mess around in ielm.
(transient--parse-child 'magit-dispatch '("a" "action" (lambda () (interactive) (message "hey"))))
Note you can replace transient--prefix
with tsc-generated-group
in the
example above. transient--prefix
is just a variable that happens to hold
the prefix during layout.
- These functions do mostly the same job. Why do we need to specify a
prefix for
transient-parse-suffixes
, for scope etc?
Basically you are on your own. Just call (oref transient--prefix scope)
during layout setup or (oref transient-current-prefix)
during suffix
bodies.
Because suffixes are basically also commands (riding in the same symbol plist), a suffix can be called independently. In this case, if its expecting to read the scope from the prefix when there is no prefix, it might fail.
Therefore, a method called transient-init-scope
can be overridden and used
at the correct point in the lifecycle for the suffix to correct the issue.
Note, the behavior is actually quite ad-hoc. You will read the prefix yourself and then decide if you want to use some fallback.
There is a perfectly short example in Magit source for the custom
magit--git-variable
subclass of the transient-variable
infix.
Each infix instance is declared in transient-define-infix
, potentially with
a :scope
slot, possibly holding a function.
If it’s holding a function, that function will be used as a backup during
initialization in case there is no prefix or it has nothing in its scope
slot.
Not everything is a string or boolean. You may want to represent complex objects in your transint infixes. If your objects can be rehydrated from some serialized ID, you may want history support.
If you need to set and display a custom type, use the simple OOP techniques of EIEIO. Also check the suffix value methods section of the transient manual.
Essential behaviors for your custom infix:
- Defining a reader to set the infix with user input
prompt
slot’s default form,initform
for asking the user for inputtransient-init-value
to rehydrate saved valuestransient-infix-value
so that setting & saving persist what you want to rehydratetransient-format-value
to display a user-meaningful form for your value
We will also use some layout introspection:
transient-get-suffix
To get suffix by key, location, or command symbol- Getting a description from raw layout children (not EIEIO objects). See Layout Hacking.
;; The children we will be picking can be of several forms. The
;; transient--layout symbol property of a prefix is a vector of vectors, lists,
;; and strings. It's not the actual eieio types or we would use
;; `transient-format-description' to just ask them for the descriptions.
(defun tsc--layout-child-desc (layout-child)
"Get the description from LAYOUT-CHILD.
LAYOUT-CHILD is a transient layout vector or list."
(let ((description
(cond
((vectorp layout-child) (or (plist-get (aref layout-child 2) :description) "<group, no desc>")) ; group
((stringp layout-child) layout-child) ; plain-text child
((listp layout-child) (plist-get (elt layout-child 2) :description)) ; suffix
(t (message (propertize "You traversed into a child's list elements!" 'face 'warning))
(format "(child's interior) element: %s" layout-child)))))
(cond
;; The description is sometimes a callable function with no arguments,
;; so let's call it in that case. Note, the description may be
;; designed for one point in the transient's lifecycle but we could
;; call it in a different one, causing its behavior to change.
((functionp description) (apply description))
(t description))))
;; We repeat the read using a lisp expression from `read-from-minibuffer' to get
;; the LOC key for `transient-get-suffix' until we get a valid result. This
;; ensures we don't store an invalid LOC.
(defun tsc-child-infix--reader (prompt initial-input history)
"Read a location and check that it exists within the current transient.
PROMPT, INITIAL-INPUT, and HISTORY are forwarded to `read-from-minibuffer'."
(let ((command (oref transient--prefix command))
(success nil))
(while (not success)
(let* ((loc (read (read-from-minibuffer prompt initial-input nil nil history)))
(child (ignore-errors (transient-get-suffix command loc))))
(if child (setq success loc)
(message (propertize
(format
"Location could not be found in prefix %s"
command)
'face 'error))
(sit-for 3))))
success))
;; Inherit from variable abstract class
(defclass tsc-child-infix (transient-variable)
((value-object :initarg value-object :initform nil)
;; this is a new slot for storing the hydrated value. we re-use the
;; value infrastructure for storing the serialization-friendly value,
;; which is basically a suffix addres or id.
(reader :initform #'tsc-child-infix--reader)
(prompt :initform "Location, a key \"c\", suffix-command-symbol like tsc--wave-normally or coordinates like (0 2 0): ")))
;; We have to define this on non-abstract infix classes. See
;; `transient-init-value' in transient source. The method on
;; `transient-argument' class is the best example for initializing your
;; suffix based on the prefix's value, but it does support a lot of
;; behaviors.
(cl-defmethod transient-init-value ((obj tsc-child-infix))
"Set the `value' and `value-object' slots using the prefix's value."
(let* ((prefix-value (oref transient--prefix value))
(key (oref obj command))
(value (car (alist-get key prefix-value))) ; car?
(value-object (transient-get-suffix (oref transient--prefix command) value)))
(oset obj value value)
(oset obj value-object value-object)))
(cl-defmethod transient-infix-set ((obj tsc-child-infix) value)
"Update `value' slot to VALUE.
Update `value-object' slot to the value corresponding to VALUE."
(let* ((command (oref transient--prefix command))
(child (ignore-errors (transient-get-suffix command value))))
(oset obj value-object child)
(oset obj value (if child value nil))))
;; If you are making a suffix that needs history, you need to define this
;; method. You also need this method if your value needs some processing
;; or use of an alternate value for later rehydration. Tell the prefix
;; what to store when setting / saving
(cl-defmethod transient-infix-value ((obj tsc-child-infix))
"Return our actual value for rehydration later."
;; this is almost identical to the method defined for `transient-infix',
;; but don't forget this if you want history on a suffix for example.
(list (oref obj command) (oref obj value)))
;; Show user's a useful representation of your ugly value
(cl-defmethod transient-format-value ((obj tsc-child-infix))
"All transient children have some description we can display.
Show either the child's description or a default if no child is selected."
(if-let* ((value (and (slot-boundp obj 'value) (oref obj value)))
(value-object (and (slot-boundp obj 'value-object)
(oref obj value-object))))
(propertize
(format "(%s)" (tsc--layout-child-desc value-object))
'face 'transient-value)
(propertize "¯\_(ツ)_/¯" 'face 'transient-inactive-value)))
;; Now that we have our class defined, we can create an infix the usual
;; way, just specifying our class
(transient-define-infix tsc--inception-child-infix ()
:class tsc-child-infix)
;; All set! This transient just tests our or new toy.
(transient-define-prefix tsc-inception ()
"Prefix that picks a suffix from its own layout."
[["Pick a suffix"
("-s" "just a switch" "--switch") ; makes history value structure apparent
("c" "child" tsc--inception-child-infix :class tsc-child-infix)]
["Some suffixes"
("s" "wave surely" tsc--wave-surely)
("d" "wave definitely" tsc--wave-definitely)
("e" "wave eventually" tsc--wave-eventually)
("C" "call & exit normally" tsc--wave-normally :transient nil)]
["Read variables"
("r" "read args" tsc-suffix-print-args )]])
;; (tsc-inception)
;; Try setting the infix to "e" (yes, include quotes)
;; Try: (1 2)
;; Try: tsc--wave-normally
;; Set the infix and re-open it
;; Save the infix, re-evaluate the prefix, and open the prefix again
;; Try flipping through history
;; Now do think of doing things like this with org ids, magit-sections, buffers etc.
This is a difficult example, but once you understand the pieces, you can
see some of the magit variables in action like magit--git-variable
and
it’s many subclasses.
Revisit the section on detangling setting, saving and history. Watching the values update will make it clear what representations are bing stored, where, and when.
Note, however you store and rehydrate will affect how you read, so try to
make it just work with transient-read-arg
, unlike this example (TODO).
(transient-define-suffix tsc--inception-update-description ()
"Update the description of of the selected child."
(interactive)
(let* ((args (transient-args transient-current-command))
(description (transient-arg-value "--description=" args))
;; This is the part where we read the other infix
(loc (car (cdr (assoc 'tsc--inception-child-infix args))))
(layout-child (transient-get-suffix 'tsc-inception-update loc)))
(cond
;; Once again, do different bodies based on what we found at the layout locition.
((or (listp layout-child) ; child
(vectorp layout-child) ; group
(stringp layout-child)) ; string child
(if (stringp layout-child)
(transient-replace-suffix 'tsc-inception-update loc description) ; plain-text child
(plist-put (elt layout-child 2) :description description)))
(t (message (propertize (format
"Don't know how to modify whatever is at: %s"
loc) 'face 'warning))))
;; re-enter the transient manually to display the modified layout
(transient-setup transient-current-command)))
(transient-define-prefix tsc-inception-update ()
"Prefix that picks and updates its own suffix."
[["Pick a suffix"
("c" "child" tsc--inception-child-infix)]
["Update the description!"
("-d" "description" "--description=") ; makes history value structure apparent
("u" "update" tsc--inception-update-description :transient transient--do-exit)]
["Some suffixes"
("s" "wave surely" tsc--wave-surely)
("d" "wave definitely" tsc--wave-definitely)
("e" "wave eventually" tsc--wave-eventually)
("C" "call & exit normally" tsc--wave-normally :transient nil)]
["Read variables"
("r" "read args" tsc-suffix-print-args )]])
;; (tsc-inception-update)
;; Pick a suffix,
;; Then set the description
;; Then update the suffix's you picked with the new description!
;; Using a transient to modify a transient (⊃。•́‿•̀。)⊃━✿✿✿✿✿✿
;; Try to rename a group, such as (0 0)
;; Rename the very outer group, (0)
Modifying the very outer group doesn’t quite work. It’s probably a degenrate layout object, meaning setting a description doesn’t cause it to behave like a group with a heading. Maybe outer groups have a different data structure? An exercise left to the reader
The flow control for re-display is slightly fighting the history implementation. It would be better if we could retain values while triggering a redraw without even more hacking & state manipulation.
Emacs lisp ships with eieio, a close cousin to the Common Lisp Object System. It’s OOP. There are classes & subclasses. You can inherit into new classes and override methods to customize behaviors.
You can use eieio API’s to explore transient objects. Let’s look at some transients you have already:
;; The plist for a prefix command contains a `transient-prefix' object in the
;; `transient--prefix' key and a vector layout in `transient--layout' (symbol-plist
(symbol-plist 'magit-dispatch)
;; getting the values from the symbol plist
(plist-get (symbol-plist 'magit-dispatch) 'transient--prefix)
(let ((prefix-object (plist-get (symbol-plist 'magit-dispatch) 'transient--prefix)))
;; printing the current slot values for that object
(object-write prefix-object)
;; ;; Object transient-prefix-20997da
;; (transient-prefix "transient-prefix-20997da"
;; :command magit-dispatch :info-manual "(magit)Top")
;; getting the class of an object
(eieio-object-class prefix-object) ; transient-prefix
;; opening the help documents for the class, which shows all methods and
;; slot forms
(describe-function transient-prefix))
Like all OOP, the three things you want to do are:
cl-defmethod
and sometimes cl-call-next-method
Inside the defclass
form, you can set slots that you don’t like.
:initform
is a default value. :initarg
configures which argument
to pick up from the class constructor.
oref
and oset
(method-name object arguments)
See methods like slot-boundp
in the EIEIO eieio method index
Here’s a list of all of transient’s defclass
and their ancestry. This is
how it is in 2022.
(eieio-browse) ; shows all known classes and their ancestry
;; +--transient-child
;; | +--transient-group
;; | | +--transient-subgroups
;; | | +--transient-columns
;; | | +--transient-row
;; | | +--transient-column
;; | +--transient-suffix
;; | +--magit--git-submodule-suffix
;; | +--transient-infix
;; | +--transient-variable
;; | | +--magit--git-variable
;; | | | +--magit--git-branch:upstream
;; | | | +--magit--git-variable:urls
;; | | | +--magit--git-variable:choices
;; | | | +--magit--git-variable:boolean
;; | | +--transient-lisp-variable
;; | +--transient-argument
;; | +--transient-switches
;; | +--transient-option
;; | | +--transient-files
;; | +--transient-switch
;; +--transient-prefix
;; +--magit-log-prefix
;; | +--magit-log-refresh-prefix
;; +--magit-diff-prefix
;; +--magit-diff-refresh-prefix
Using describe-function
is extremely handly for viewing the class slots
and methods.
Classes used in transient that you are likely to want to know the slots for:
transient-prefix transient-suffix transient-infix transient-argument
The eieio docs have a more wordy treatment. The class system has a lot of behavior that can be faster at times to just understand through description.
There is a lot of support for both print-line and step-through debugging.
Just set transient--debug
to t. (setq transient-debug t)
You will get a lot of logs visible in *Messages*
via
view-echo-message-area the next time you run a transient.
-- setup (cmd: tsc-layout-rows-explicit, event: "M-x", exit: nil)
-- stack-zap (cmd: tsc-layout-rows-explicit, event: "M-x", exit: nil)
-- init-transient (cmd: tsc-layout-rows-explicit, event: "M-x", exit: nil)
push transient--transient-map
push transient--redisplay-map
-- post-command (cmd: tsc-layout-rows-explicit, event: "M-x", exit: nil)
-- pre-command (cmd: transient-update, event: "w", exit: nil)
pop transient--redisplay-map
-- post-command (cmd: transient-update, event: "w", exit: nil)
pop transient--redisplay-map
push transient--redisplay-map
-- pre-command (cmd: tsc-suffix-wave, event: "w l", exit: nil)
-- stack-zap (cmd: tsc-suffix-wave, event: "w l", exit: nil)
-- pre-exit (cmd: tsc-suffix-wave, event: "w l", exit: t)
pop transient--transient-map
pop transient--redisplay-map
Waves at the user at: Sat Nov 12 22:38:20 2022.
-- post-command (cmd: tsc-suffix-wave, event: "w l", exit: t)
-- post-exit (cmd: tsc-suffix-wave, event: "w l", exit: t)
Edebug works with transients. There is much support in transient to facilitate using edebug.
For watching the flow control around your command, especially helpful for debugging behavior around setup, layout, or suffix dispatch, you might want to watch your transient in Edebug.
Edebug basic introduction video (10 min).
In short:
- goto your transient source
- instrument a function you want to watch with
edebug-defun
- call the transient / suffix that triggers entry of that function
- use
SPC
to step forward,c
to continue,i
to enter a function call, orh
for help etc
First watch the debug output to gain an idea of how your code flows with the
transient code. Then instrument transient behaviors such as
transient--post-exit
and use i
to edebug-step-in
to calls of interest.
When you are done, remember to use =edebug-remove-instrumentation= so that you can go on without every transient you open trying to call the debugger.
Because edebug works on defuns while suffixes are defined with macros, you may need to macro exand in order to come up with something debuggable.
First you need to explort the layout data structures.
;; Let's look at the layout
(let ((prefix-layout (plist-get (symbol-plist 'magit-dispatch) 'transient--layout)))
(type-of prefix-layout) ; cons
(listp prefix-layout) ; t
(length prefix-layout) ; 3
;; Each group in the list is a vector
(vectorp (car prefix-layout)) ; t
(elt (car prefix-layout) 0) ; first element is a priority
(elt (car prefix-layout) 1) ; second is a type name
(elt (car prefix-layout) 2) ; contents & attributes
;; the attributes are key-value pairs used to create the class
;; instance when the transient is shown.
;; the nested contents will be lists of vectors for groups and
;; lists of lists for suffixes.
)
;; A sample layout
;; ([1 transient-column nil
;; ((1 transient-suffix
;; (:key "i" :description "Ignore" :command magit-gitignore))
;; (1 transient-suffix
;; (:key "I" :description "Init" :command magit-init))
;; (1 transient-suffix
;; (:key "j" :description "Jump to section" :command magit-status-jump :if-mode magit-status-mode))
;; (1 transient-suffix
;; (:key "j" :description "Display status" :command magit-status-quick :if-not-mode magit-status-mode)))])
You might find this helpful when constructing dynamic layouts
Just a reminder, some hooks exist. Use describe-variable
and complete with
transient hook
for the most recent list of hooks.
Definitions that are not that interesting on their own but are used in examples.
(defun tsc-suffix-wave ()
"Wave at the user."
(interactive)
(message "Waves at the user at: %s." (current-time-string)))
(transient-define-suffix tsc-suffix-show-level ()
"Show the current transient's level."
:transient t
(interactive)
(message "Current level: %s" (oref transient-current-prefix level)))
;; Because command names are used to store and lookup child levels, we have
;; define a macro to generate unqiquely named wavers. See #153 at
;; https://github.com/magit/transient/issues/153
(defmacro tsc--define-waver (name)
"Define a new suffix with NAME tsc--wave-NAME."
`(transient-define-suffix ,(intern (format "tsc--wave-%s" name)) ()
,(format "Wave at the user %s" name)
:transient t
(interactive)
(message (format "Waves at %s" (current-time-string)))))
;; Each form results in a unique suffix definition.
(tsc--define-waver "surely")
(tsc--define-waver "normally")
(tsc--define-waver "non-essentially")
(tsc--define-waver "definitely")
(tsc--define-waver "eventually")
(tsc--define-waver "hidden")
Here’s a suffix that reads the transient’s infix values, the prefix’s
scope, and any universal argument (C-u 4
etc).
(transient-define-suffix tsc-suffix-print-args (the-prefix-arg)
"Report the PREFIX-ARG, prefix's scope, and infix values."
:transient 'transient--do-call
(interactive "P")
(let ((args (transient-args (oref transient-current-prefix command)))
(scope (oref transient-current-prefix scope)))
(message "prefix-arg: %s \nprefix's scope value: %s \ntransient-args: %s"
the-prefix-arg scope args)))
;; tsc-suffix-print-args command is incidentally created
If you were hit in the face with the first example, you need to learn basic Elisp. This is not an Elisp guide. Here’s some starting points:
- transient-define-prefix is a macro that creates a command and attaches a
transient-prefix
object to the command symbol’s plist. - lambda is a macro to create an anonymous function
- interactive is a macro that makes the function compatible with the command
interface, the
M-x
orexecute-extended-command
menu - The brackets are just vector syntax.
Besides the other ways to evaluate elisp used in this README, try
ielm
.Use the built-in elisp manual by calling the command
elisp-index-search
. See shortdocs for functions usingshortdoc-display-groups
.The EIEIO and CL manuals are independent from the Elisp manual for some reason. EIEIO is pretty short and not used much once you get the hang of it.
info-display-manual
Common Lisp manual you don’t really need the common lisp manual for working with transient. Don’t be alarmed when you see EIEIO using functions like
cl-call-next-method
- *The Transient Manual* (web link) contains more detailed explanation of behavior. The examples here should allow you to visualize what is being described. This guide and the manual should be your first and second sources.
- *Transient source* (web link) is all in one file. Source code is always more accurate than manual descriptions, even if some behavior implementations are a bit scattered.
- *Magit source* (web link) contains numerous examples of transient being used in a big, full-feature application. Search the source for “transient” and you will find many prefixes, suffixes, and custom classes. The smallest examples may be harder to find and most combine many behaviors at once.
<<package-footer>>
The headers and footers for the tangled module.
;;; transient-showcase.el --- transient features & behavior showcase -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Positron Solutions
;; Author: Psionik K <[email protected]>
;; Keywords: convenience
;; Version: 0.1.0
;; Package-Requires: ((emacs "28.1"))
;; Homepage: http://github.com/positron-solutions/transient-showcase
;;; License notice:
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This package is created from the README and serves as a fast way to load
;; all of the examples without tangling the org document. This is appropriate
;; if you just want to quickly browse through the examples and see their
;; source code.
;;
;; M-x tsc-showcase contains most of the prefixes and can be bound for
;; use as a quick reference. Just use transient's help for each
;; command to see the source. C-h <suffix key>.
;;
;;; Code:
(require 'transient)
(require 'org-id)
This block includes the showcase transient.
(transient-define-prefix tsc-showcase ()
"A launcher for a currated selection of examples.
While most of the prefixes have their :transient slot set to t, it's not
possible to return from all of them, especially if they demonstrate flow
control such as replacing or exiting."
[["Layouts"
("ls" "stacked" tsc-layout-stacked :transient t)
("lc" "columns" tsc-layout-columns :transient t)
("lt" "stacked columns" tsc-layout-stacked-columns :transient t)
("lg" "grid" tsc-layout-the-grid :transient t)
("lp" "spaced out" tsc-layout-spaced-out :transient t)
("le" "explicit class" tsc-layout-explicit-classes :transient t)
("ld" "descriptions" tsc-layout-descriptions :transient t)
;; padded description to sc
("lD" "dynamic descriptions " tsc-layout-dynamic-descriptions :transient t)]
["Nesting & Flow Control"
("fs" "stay transient" tsc-stay-transient :transient t)
("fb" "binding sub-prefix" tsc-simple-parent :transient t)
("fr" "sub-prefix with return" tsc-simple-parent-with-return :transient t)
("fm" "manual setup in suffix" tsc-parent-with-setup-suffix :transient t)
("fi" "mixing interactive" tsc-interactive-basic :transient t)
("fe" "early return" tsc-simple-messager :transient t)]]
[["Managing State" ; padded right group
("sb" "a bunch of infixes" tsc-basic-infixes :transient t)
("sc" "using scope (accepts prefix)" tsc-scope :transient t)
("sn" "set & save / snowcones" tsc-snowcone-eater :transient t)
("sp" "history key / ping-pong" tsc-ping :transient t)
("sg" "always forget / goldfish" tsc-goldfish :transient t)
("se" "always remember / elephant" tsc-elephant :transient t)
("sd" "default values" tsc-default-values :transient t)
("sf" "enforcing inputs" tsc-enforcing-inputs :transient t)
("sl" "lisp variables" tsc-lisp-variable :transient t)]
["CLI arguments"
("cb" "basic arguments" tsc-switches-and-arguments :transient t)
("cm" "random-init infix" tsc-maybe-on :transient t)
("cc" "basic choices" tsc-animal-choices :transient t)
("ce" "exclusive switches" tsc-exclusive-switches :transient t)
("ci" "incompatible switches" tsc-incompatible :transient t)
("co" "completions for choices" tsc-choices-with-completions :transient t)
("cc" "cowsay cli wrapper" tsc-cowsay :transient t)]]
[["Visibility"
;; padded description to sc
("vp" "predicates " tsc-visibility-predicates :transient t)
("vi" "inapt (not suitable)" tsc-inapt-parent :transient t)
("vl" "levels" tsc-levels-and-visibility :transient t)]
["Advanced"
("ac" "generated child" tsc-generated-child :transient t)
("ag" "generated group" tsc-generated-group :transient t)
("ai" "custom infixes" tsc-inception :transient t)
("au" "custom infixes & update" tsc-inception-update :transient t)]])
(provide 'transient-showcase)
;;; transient-showcase.el ends here