-
-
Notifications
You must be signed in to change notification settings - Fork 66
Developer Quick Start Guide
This guide assumes you have minimal knowledge of Emacs, some programming
experience in lisp and non-lisp languages, and have at least seen screenshots
of magit
.
This wiki page is edited in Org mode, so you can copy contents from the edit
page into a buffer, set to org mode, and then run individual source blocks with
org-babel-execute-src-blk
Transient gets it’s name from the non-persistent keymap and the popup UI for displaying that keymap. It is a transient, ie temporary UI. The most basic user flow looks like this:
Prefix -> Suffix
- The prefix is the command you invoke first, such as
magit-dispatch
- A suffix is a bound command displayed in the UI, such as
magit-stage
The entire ensemble (UI & keymap) is frequently referred to as “a transient”. “Prefix” and “a transient” are almost the same thing. Invoking a prefix will show a transient.
A prefix can also be bound as a suffix, enabling nested prefixes. This is what happens when, in the
magit-dispatch
transient (?
), you select l
for magit-log
. When calling
nested transients, the sentences look like this:
Prefix -> Sub-Prefix -> Sub-Prefix -> Suffix
Some suffixes need to hold state, toggling or storing an argument. Infixes are specialized suffixes made for this purpose. A full command sentence with infixes may look like this:
Prefix -> Infix -> Infix -> Suffix
- Prefixes display the pop-up UI and bind the keymap.
- Suffixes are bound within a prefix
- Infixes are a specialized suffix with easy support in the API for storing state
- A Suffix may be a new Prefix, in which case the transient is nested
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.
When defining a transient prefix or suffix, you will see a lot of :property
value
pairs. This is not a syntax. It’s just vanilla lists. Macros like
define-transient-prefix
will read the pairs into a list and use them to
create objects with slots set to those property values.
You can use eieio API’s to explore transient objects. Let’s look at some transients you have already:
;; 'transient--prefix object is stored in symbol properties
(setq prefix-object (plist-get (symbol-plist 'magit-log) 'transient--prefix))
;; get the class of the object
(setq prefix-class (eieio-object-class prefix-object))
;; get the slots for that class, returns a list of structs
(eieio-class-slots prefix-class)
;; print some basic information about slots & methods
(eieio-help-class prefix-class)
(transient-define-prefix transient-toys-hello ()
"Say hello"
[("h" "hello" (lambda () (interactive) (message "hello")))])
(transient-toys-hello)
Executing the source block will display the transient, but you can also
execute-extended-command
(M-x) and select transient-toys-hello
to view
this transient. All transients are commands and all of the actions are also
commands.
There are 2-3 ways to achieve most behaviors.
- Compact forms in
transient-define-prefix
allow shorthand definitions of suffixes and their bindings - Property lists handle more extensive declarations
- Macros are available to separate out repetitive definitions or clean up later prefix bindings
The ("key" "description" suffix-or-command-symbol)
form within a group is
extremely common.
Note: Any command 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.
(defun transient-toys--wave ()
"Wave at the user"
(interactive)
(message (propertize
(format "Waves at %s" (current-time-string))
'face 'success)))
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
[("w" "wave" transient-toys--wave)])
(transient-toys-wave)
Not all behaviors have a compact form, so as you use more behaviors, you will
see more of the property style API. Here we use the :transient
property,
set to true, meaning the suffix won’t exit the transient.
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
[("w" "wave" transient-toys--wave :transient t)])
(transient-toys-wave)
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 transient-toys--wave ()
"Wave at the user"
:transient t
:key "C-w"
:description "wave"
(interactive)
(message (propertize
(format "Waves at %s" (current-time-string))
'face 'success)))
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
[(transient-toys--wave)])
(transient-toys-wave)
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
transient-toys--wave
suffix defined above:
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
[(transient-toys--wave :transient nil :key "wf" :description "wave furiously")])
(transient-toys-wave)
If you just list the key and symbol followed by properties, it is also a supported compact suffix form:
("wf" transient-toys--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 compact style :property symbol
out to the transient-define-suffix
form, 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.
There is no transient-define-group
at this time.
The default behavior treats groups a little differently depending on how they are nested. For most simple groupings, this is sufficient control.
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
["Top Group" ("wo" "wave one" transient-toys--wave)]
["Bottom Group" ("wt" "wave two" transient-toys--wave)])
(transient-toys-wave)
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
[["Left Group" ("wo" "wave one" transient-toys--wave)]
["Right Group" ("wt" "wave two" transient-toys--wave)]])
(transient-toys-wave)
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
["Top Group" ("wo" "wave one" transient-toys--wave)]
[["Left Group" ("wt" "wave two" transient-toys--wave)]
["Right Group" ("wa" "wave all" transient-toys--wave)]])
(transient-toys-wave)
Very straightforward. Just make the first element in the vector a string or
add a :description
property, which can be a function.
In the compact form of declaring suffixes, the second string is a description.
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
["Group One"
("wo" "wave one" transient-toys--wave)
("we" "wave emotionally" transient-toys--wave)]
["Group Two"
("ws" "wave some" transient-toys--wave)
("wb" "wave better" transient-toys--wave)]
[["Group Three" ("wt" "wave two" transient-toys--wave)]
["Group Four" ("wa" "wave all" transient-toys--wave)]])
(transient-toys-wave)
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.
(require 'cl-lib)
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
["Group One"
("wo" "wave one" transient-toys--wave)
("we" "wave emotionally" transient-toys--wave)]
[:description current-time-string
("ws" transient-toys--wave
:description (lambda ()
(format "Wave at %s" (current-time-string))))
("wb" "wave better" transient-toys--wave)]
[[:description (lambda () (format "Group %s" (cl-gensym)))
("wt" "wave two" transient-toys--wave)]
[:description (lambda () (format "Group %s" (cl-gensym)))
("wa" "wave all" transient-toys--wave)]])
(transient-toys-wave)
Functions need arguments. Infixes are specialized suffixes with behavior defaults that make sense for setting values. They have interfaces for persisting state across invocations.
The compact style is heavily influenced by the CLI style switches and arguments that transient was built to control. The most frequent short form is:
("key" "description" "argument")
The default behavior will change the infix type 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
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 transient-toys-wave ()
"Wave at the user"
[["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"
("ws" "wave some" transient-toys--wave)
("wb" "wave better" transient-toys--wave)])
(transient-toys-wave)
Every transient prefix has a value. It’s a list. You can set it to create defaults for switches and arguments.
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
:value '("--toggle" "--value=default")
["Arguments"
("-s" "switch" "--switch")
("-a" "argument" "--argument=")
("t" "toggle" "--toggle")
("v" "value" "--value=")]
["Commands"
("ws" "wave some" transient-toys--wave)
("wb" "wave better" transient-toys--wave)])
(transient-toys-wave)
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
).
Sometimes the :shortarg
doesn’t exactly match the :key:
and :argument
,
so it can be specified manually.
If you need to fine-tune a switch, 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.
In the example below, :init-value
is defined to randomly set the switch to
true. Every time you invoke the transient-toys-wave
prefix, there’s a
chance that the --switch
will already be set.
(transient-define-infix transient-toys--switch ()
"Switch on and off"
:argument "--switch"
:shortarg "-s" ; will be used for :key when key is not set
:description "switch"
; if you haven't seen setf, think of it as having the power to set via a getter
:init-value (lambda (ob)
(setf
(slot-value ob 'value) ; get value
(eq 0 (random 2))))) ; write t with 50% probability
(transient-define-prefix transient-toys-args ()
"Wave at the user"
["Arguments"
(transient-toys--switch)])
(transient-toys-args)
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 transient-toys--animals-argument ()
"Animal picker"
:argument "--animal="
:shortarg "-a"
:description "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 transient-toys-animals ()
"Select animal"
["Arguments"
(transient-toys--animals-argument)])
(transient-toys-animals)
The compact form of choices can be used for a compact argument. Use :class
'transient-option
if you need to force the class.
(transient-define-prefix transient-toys-animals ()
"Select animal"
["Arguments"
("-a" "Animal" "--animal=" :choices ("fox" "kitten" "peregrine" "otter"))])
(transient-toys-animals)
(defun transient-toys--animal-choices (complete-me filter-p completion-type)
;; complete-me: whatever the user has typed so far
;; filter-p: function you should use to filter candidates (only nil seen so far)
;; completion-type: t on first input and (metadata . alist) thereafter
;;
;; Documentation is from Emacs. This is not transient-specific behavior
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Programmed-Completion.html
(if (eq 0 (random 2))
'("fox" "kitten" "otter")
'("ant" "peregrine" "zebra")))
(transient-define-prefix transient-toys-animals ()
"Select animal"
["Arguments"
("-a" "Animal" "--animal="
:always-read t ; don't allow unsetting, just read a new value
:choices transient-toys--animal-choices)])
(transient-toys-animals)
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.
Switches may not have a compact form.
(transient-define-argument transient-toys--snowcone-flavor ()
:description "Flavor of snowcone"
:class 'transient-switches
:key "-s"
:argument-format "--%s-snowcone"
:argument-regexp "\\(--\\(grape\\|orange\\|cherry\\|lime\\)-snowcone\\)"
:choices '("grape" "orange" "cherry" "lime"))
(transient-define-prefix transient-toys-snowcone-eater ()
"Eat a flavored snowcone!"
:value '("--orange-snowcone")
["Arguments"
(transient-toys--snowcone-flavor)])
(transient-toys-snowcone-eater)
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 transient-toys-set-switches ()
:incompatible '(
;; update your transient version if you experience #129 / #155
("--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=")])
(transient-toys-set-switches)
The function signatures are really similar to other completing reads. Check out programmed completions
When initializing an option, you may want the variable to go obtain some
more information. This is done by :init-scope
. An example in magit
Branch configuration in magit uses this a lot.
magit branch rebase using a scope
Note that the “scope” is a string, the branch name here. It’s probably pretty easy to use other kinds of scopes.
In the case of the infix above, here’s variable’s
class
definition and also where the :variable
string on the infix gets
formatted with the scope!
Note non-updated predicate errata strikes again =(
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 inteded 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 transient-toys--position '(0 0) "A transient prefix location")
(transient-define-infix transient-toys--pos-infix ()
"A location, key, or command symbol"
:class 'transient-lisp-variable
:prompt "An expression such as (0 0), \"k\", 'my-suffix"
:variable 'transient-toys--position)
(transient-define-suffix transient-toys--msg-pos ()
"Message the element at location"
:if-non-nil 'transient-toys--position
:transient 'transient--do-call
(interactive)
;; lisp variables are not sent in the usual (transient-args) list. Just read
;; the value.
(message (concat (propertize "object at loc: \n" 'face 'success)
(format "%s" (transient-get-suffix transient-current-command
transient-toys--position)))))
(transient-define-prefix transient-toys-msg-location ()
"Message with the object at a location"
["Location Printing"
[("p" "position" transient-toys--pos-infix)]
[("m" "message" transient-toys--msg-pos)]])
(transient-toys-msg-location)
If you need to set and display a custom type, the fastest way is through
eieio and applying a bit of simple OOP. :initform
is a default value.
:initarg
configures which argument to pick up from the class constructor.
The
eieio docs have some simple examples that should quickly get you up to
speed.
Let’s write a transient menu that can interrogate transient prefixes! We need to be able to pick a child from the prefix’s layout and then get information about its properties and finally to set the properties.
First we need an infix to pick, store, and display the suffix or group we are operating on.
We will use several new pieces here:
-
transient-get-suffix
To get suffix by a key, location, or command symbol -
transient-format-description
Method works on children to get their description string
;; 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'
(defun transient-toys--layout-child-desc (layout-child)
"Get the description from a transient layout vector or list."
(cond
((vectorp layout-child) (or (plist-get (aref layout-child 2) :description) "<group>")) ; group
((stringp layout-child) layout-child) ; plain-text child
((listp layout-child) (plist-get (elt layout-child 2) :description)) ; suffix
(t (format "idk: %s" layout-child))))
;; Inherit from variable abstract class
(defclass transient-child-variable (transient-variable)
((reader :initform #'transient-child-variable--reader )
(transient :initform 'transient--do-call))) ; we want access to transient-current-command
;; We have to define this on non-abstract infix classes. See
;; `transient-init-value' in transient source.
(cl-defmethod transient-init-value ((obj transient-child-variable))
(oset obj value nil))
(cl-defmethod transient-prompt ((obj transient-child-variable))
"Location, a key \"a\", 'suffix-command, or coordinates (0 0 1): ")
;; Customize how we display our value since it's actual value is ugly
(cl-defmethod transient-format-value ((obj transient-child-variable))
"All transient children have some description we can display.
Show either the child's description or a default if no child is selected."
(let ((value (if (slot-boundp obj 'value) (slot-value obj 'value) nil)))
(if value
(propertize
(format "(%s)" (transient-toys--layout-child-desc value))
'face 'transient-value)
(propertize "¯\_(ツ)_/¯" 'face 'transient-inactive-value))))
;; 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 transient-child-variable--reader (prompt initial-input history)
"Read a location and check that it exists within the current transient."
(save-match-data
(cl-block nil ; allows cl-return
(while t
;; read a string, then read it as a lisp object
(let* ((loc (read (read-from-minibuffer prompt initial-input nil nil history)))
(child (ignore-errors (transient-get-suffix transient-current-command loc))))
(when child
(cl-return child)) ; breaks loop
(message
(propertize
(format "Location could not be found in prefix %s"
transient-current-command) 'face 'error))
(sit-for 1)))))) ; wait a second
;; TODO really wish I don't need explicit infix declation
(transient-define-infix transient-toys--inception-child ()
:class transient-child-variable)
;; All set! This transient just tests our or new toy.
(transient-define-prefix transient-toys-inception-set ()
"Pick a suffix, any suffix"
[["Pick"
("c" "child" transient-toys--inception-child :class transient-child-variable)]])
(transient-toys-inception-set)
Setting values is cool, but we want to use them. Transient variables don’t
show up in transient-arguments
calls. This is fine because those are
pretty specific to CLI building.
We want to set arbitrary properties on a an arbitrary child. To do this, we
need to retain the LOC for making transient-suffix-put
and also to get the
available slots of the suffix and group objects.
History keys can be used to make unique or shared history for values.
Search for transient-set
and transient-save
in magit.
Also see the manual.
Magit defining some classes of prefixes with a single shared history key
Magit defining history key for an infix
I have no idea where corresponding behavior can be found.
So far we have focused on setting arguments and dispatching commands. Now we put the two together.
As you should be familiar with in (interactive)
forms, commands in Emacs
might be called from another function or may be called by the user. Either
they are given the arguments or they must go find them.
Transient does not change this model. When a suffix command is called, it has to go find the arguments of the prefix command that called it.
Read up on the built-in interactive behavior if you are unfamiliar. Interactive codes for writing your own intuitive user inputs are extremely valuable on their own!
Remember that each transient has a :value
. 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.
;; By writing the command using interactive style, you can still call the
;; function manually from elisp in order to pass in your own arguments, and it
;; will skip trying to get the arguments from the user or the current transient.
(transient-define-suffix transient-toys--msg-args (&optional args)
"Show current infix args"
:key "a"
:description "show arguments"
:transient t ; be sure you are using later than be119ee43fe5f6af2469d366f6ab84204255038d
(interactive (list (transient-args transient-current-command)))
(message (concat (propertize "Current args: " 'face 'success)
(format "%s" args))))
(transient-define-suffix transient-toys--eat-snowcone (&optional args)
"Eat the snowcone!"
:key "e"
:description "eat snowcone"
(interactive (list (transient-args transient-current-command)))
(let ((topping (transient-arg-value "--topping=" args))
(flavor (transient-arg-value "--flavor=" args)))
(message (concat (propertize
(format "I ate a %s flavored snowcone with %s on top!" flavor topping)
'face 'success)))))
(transient-define-prefix transient-toys-snowcone-eater ()
"Eat a flavored snowcone!"
:value '("--topping=fruit" "--flavor=cherry")
["Arguments"
("-t" "topping" "--topping=" :choices ("ice cream" "fruit" "whipped cream" "mochi"))
("-f" "flavor" "--flavor=" :choices ("grape" "orange" "cherry" "lime"))]
["Actions"
(transient-toys--msg-args)
(transient-toys--eat-snowcone)])
(transient-toys-snowcone-eater)
*Currently it appears that, due to behavior, it’s not easy to get the
arguments for a distant command in a sequence of multiple prefixes because
exporting one prefix causes calling transient-args
on another returns
=nil=*
You may be tempted to look at transient--stack
but this variable is more
related to suspend & resume (in addition to being from the private API).
(defun magit-branch-arguments ()
(transient-args 'magit-branch))
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. Message is never actually passed as message=
, So we end up
stripping it from the arguments and re-assembling something call-process
can use.
Note cowsay supports more options, but for the sake of keeping this example small (and to refocus effort on transient itself), the entire CLI is not wrapped.
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 transient-toys--quit-cowsay ()
"Kill the cowsay buffer and exit"
(interactive)
(kill-buffer "*cowsay*"))
(defun transient-toys--cowsay-buffer-exists-p ()
(not (equal (get-buffer "*cowsay*") nil)))
(transient-define-suffix transient-toys--cowsay-clear-buffer (&optional buffer)
"Delete the *cowsay* buffer. Optional BUFFER name."
:transient 'transient--do-call
:if 'transient-toys--cowsay-buffer-exists-p
(interactive) ; we don't use "b" interactive code because default is known
(save-excursion
(let ((buffer (or buffer "*cowsay*")))
(set-buffer buffer)
(delete-region 1 (+ 1 (buffer-size))))))
(transient-define-suffix transient-toys--cowsay (&optional args)
"Run cowsay"
(interactive (list (transient-args transient-current-command)))
(let* ((buffer "*cowsay*")
(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 (transient-toys--cowsay-buffer-exists-p)
(transient-toys--cowsay-clear-buffer))
(apply #'call-process "cowsay" nil buffer nil args)
(switch-to-buffer buffer)))
(transient-define-prefix transient-toys-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" transient-toys--cowsay :transient transient--do-call)
""
("d" "delete buffer" transient-toys--cowsay-clear-buffer)
("q" "quit" transient-toys--quit-cowsay)]])
(transient-toys-cowsay)
You don’t always want to build trees of commands. Sometimes you don’t need to show another level to complete a command. Sometimes you want to return to a previous level.
Before a command body runs, a pre-command is what sets up the state for that
command to run in (transient-current-prefix
transient-current-suffixes
etc). The pre-command also currently configures what will happen in
post-command. *The pre-command affects behavior both before and after your
command body, so it’s safe to think of it as configuring both the entry state
setup and after-return behavior*
This example is a sentence builder. It uses both commands that “stay” transient and those that exit.
;; need this loaded
(require 'notifications)
;; We're going to construct a sentence with a transient. This is where it's stored.
(defvar transient-toys---sentence "let's transient!"
"Sentence under construction.")
;; This prefix displays the value of `transient-toys---sentence' and sets it
;; interactively (using regular lisp variables, not infix values)
(transient-define-suffix transient-toys-sentence (sentence)
"Set the sentence from minibuffer read"
:transient t
:description '(lambda () (concat "set sentence: "
(propertize
(format "%s" transient-toys---sentence)
'face 'transient-argument)))
(interactive (list (read-string "Sentence: " transient-toys---sentence)))
(setf transient-toys---sentence sentence))
;; Next we define some update commands. We don't want these commands to dismiss
;; the transient, so we set their `:transient' slot to t for `transient--do-stay'.
;; https://github.com/magit/transient/blob/master/docs/transient.org#transient-state
(transient-define-suffix transient-toys-append-dot ()
"Append a dot to current sentence"
:description "append dot"
:transient t ; true equates to `transient--do-call'
(interactive)
(setf transient-toys---sentence (concat transient-toys---sentence "•")))
(transient-define-suffix transient-toys-append-snowman ()
"Append a snowman to current sentence"
:description "append snowman"
:transient t
(interactive)
(setf transient-toys---sentence (concat transient-toys---sentence "☃")))
(transient-define-suffix transient-toys-clear ()
"Clear current sentence"
:description "clear"
:transient t
(interactive)
(setf transient-toys---sentence ""))
;; Now we want to consume our sentence. These commands are the terminal verbs
;; of our sentence construction, so they use the default `transient-do-exit'
;; behavior.
(transient-define-suffix transient-toys-message ()
"Send the constructed sentence in a message"
:description "show sentence"
;; nil sets the default `transient--do-exit' behavior
;; :transient nil
(interactive)
(message "constructed sentence: %s" (propertize transient-toys---sentence 'face 'transient-argument))
(setf transient-toys---sentence ""))
(transient-define-suffix transient-toys-notify ()
"Notify with constructed sentence"
:description "notify sentence"
(interactive)
(notifications-notify :title "Constructed Sentence:" :body
transient-toys---sentence)
(setf transient-toys---sentence ""))
(transient-define-prefix transient-toys-sentence-toy ()
"Create a sentence with several objects and a verb"
["Sentence Toy!"
("SPC" transient-toys-sentence)]
[["Transient Suffixes"
("d" transient-toys-append-dot)
("s" transient-toys-append-snowman)
"" ; empty string inserts a gap, visually separating the appends from the clear
("c" transient-toys-clear)]
["Non-Transient Suffixes"
("m" transient-toys-message)
("n" transient-toys-notify)]])
(transient-toys-sentence-toy)
A prefix can choose to display itself or can also, if it’s able to complete the work without further user interaction, return early without any display.
transient-define-prefix
can have a body. It’s an interactive command.
Within that body, if you decline to call transient-setup
then your prefix
will return early.
In the following example:
- suffix to toggle verbosity
- prefix that can return early when verbosity is off
- parent prefix to dispatch the two
Currently there is a slight lack of support in “return” behavior and it’s
not straightforward to make transient-toys--message
“return” both from its
verbose and non-verbose paths.
(defvar transient-toys--complex nil "Show verbose menu or not")
(transient-define-suffix transient-toys--toggle-verbose ()
:transient t
(interactive)
(setf transient-toys--complex (not transient-toys--complex))
(message (propertize (concat "Complexity set to: "
(if transient-toys--complex "true" "false"))
'face 'success)))
(transient-define-prefix transient-toys--message ()
["Complex Messages"
("s" "snow people" (lambda () (interactive)
(message (propertize "☃☃☃☃☃☃☃☃☃☃" 'face 'success)))
:transient transient--do-quit-one)
("r" "radiations" (lambda () (interactive)
(message (propertize "☢☢☢☢☢☢☢☢☢" 'face 'success)))
:transient transient--do-quit-one)
("k" "kitty cats" (lambda () (interactive)
(message (propertize "🐈🐈🐈🐈🐈🐈🐈🐈🐈🐈" 'face 'success)))
:transient transient--do-quit-one)]
;; The body that will either set itself up or return early
(interactive)
(if transient-toys--complex
(transient-setup 'transient-toys--message)
(message (propertize "Simple and boring!" 'face 'success))))
(transient-define-prefix transient-toys-parent ()
[["Send Message"
("m" "message" transient-toys--message)]
["Toggle Verbose"
("t" "toggle verbose" transient-toys--toggle-verbose)]])
(transient-toys-parent)
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 cannot be used.
Open the following toy in buffers with different modes (or change modes) to see the different effects of the mode predicates.
(defvar feverish nil "Are we feverish?")
(defun feverish-p ()
feverish)
(transient-define-suffix transient-toys--toggle-feverish ()
"Toggle feverish"
(interactive)
(setf feverish (not feverish))
(message (propertize (format "feverish: %s" feverish)
'face 'success)))
(defun lsp-mode-p ()
(bound-and-true-p lsp-mode))
(transient-define-prefix transient-toys-predicated ()
"Wave at the user"
["Empty Groups Not Displayed"
;; in org mode for example, this group doesn't appear
("we" "wave elisp" transient-toys--wave :if-mode emacs-lisp-mode)
("wc" "wave in C" transient-toys--wave :if-mode cc-mode)]
["Lists of Modes"
("wm" "wave multiply" transient-toys--wave :if-mode (dired-mode gnus-mode))]
[["Feverish Actions"
;; note, after toggling, the transient needs to be re-displayed for the predicate to take effect
("f" "toggle feverish" transient-toys--toggle-feverish)
("wf" "wave feverishly" transient-toys--wave :if feverish-p)]
["Programming Actions"
:if-derived prog-mode
("wp" "wave programishly" transient-toys--wave)
("wl" "wave in lsp" transient-toys--wave :if lsp-mode-p)]
["Special Mode Actions"
:if-derived special-mode
("wa" "wave all" transient-toys--wave)
("wd" "wave dired" transient-toys--wave :if-mode dired-mode)]
["Text Mode Actions"
:if-derived text-mode
("wo" "wave orgic" transient-toys--wave :if-mode org-mode)
("wx" "wave textually" transient-toys--wave)]])
(transient-toys-predicated)
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 those levels to customize what’s visible in the transient.
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.
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 customizing user to set the appropriate level.
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
. 1-7 inclusive is valid.
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.
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.
;; 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 transient-toys-define-waver (name)
"Define a new suffix named transient-toys--wave-NAME"
`(transient-define-suffix ,(intern (format "transient-toys--wave-%s" name)) ()
"Wave at the user"
:transient t
(interactive)
(message (propertize
(format "Waves at %s" (current-time-string))
'face 'success))))
(transient-toys-define-waver "surely")
(transient-toys-define-waver "normally")
(transient-toys-define-waver "non-essentially")
(transient-toys-define-waver "definitely")
(transient-toys-define-waver "eventually")
(transient-toys-define-waver "surely")
(transient-define-prefix transient-toys-levels-of-waves ()
"Wave at the user"
[["Essential Commands"
;; 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)]
[2 "Per Group" ; 1 is the default default-child-level
("ws" "wave surely" transient-toys--wave-surely) ; 1 is the default default-child-level
(3"wn" "wave normally" transient-toys--wave-normally)
(5"wb" "wave non-essentially" transient-toys--wave-non-essentially)]
[3 "Per Group Somewhat Useful"
("wd" "wave definitely" transient-toys--wave-definitely)]
[5 "Per Group Rare"
("we" "wave eventually" transient-toys--wave-eventually)]])
(transient-toys-levels-of-waves)
transient-setup-children is a prefix method that can be overriden 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.