-
-
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
.
- There are some useful links in issue #51.
- Issues with the `faq` label are also useful.
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 be bound as a suffix. 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 -> Suffix (Prefix) -> Suffix (Prefix) -> Suffix
Some suffixes are designed to hold state, toggling or storing an argument. Infixes are suffixes made for this purpose. A full command sentence 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 these values into a list and use them to
create objects with those properties.
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)
Just 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.
Any command can be used. It’s a good reason to use private--namespace
style names for suffix actions since these commands don’t 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)
There are compact forms and explicit forms. 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 so that the
transient remains …transient instead of exiting.
(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 transient 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 transient definition. This includes both the compact forms and property API’s.
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
[("fw" "Furiously wave" transient-toys--wave :transient nil)])
(transient-toys-wave)
There is basic layout support and you can use it to collect or differentiate
commands. To define a transient, you need at least one group. Groups are
vectors, delimited as [ ...group... ]
.
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"
[("wo" "wave one" transient-toys--wave)]
[("wt" "wave two" transient-toys--wave)])
(transient-toys-wave)
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
[[("wo" "wave one" transient-toys--wave)]
[("wt" "wave two" transient-toys--wave)]])
(transient-toys-wave)
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
[("wo" "wave one" transient-toys--wave)]
[[("wt" "wave two" transient-toys--wave)]
[("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.
(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 API for dynamic descriptions is the same for both prefixes and suffixes.
(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" "wave some" transient-toys--wave)
("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.
Switches are boolean. Arguments accept values.
The compact style is heavily influenced by the CLI style switches and
arguments that transient was built to control. The “=” character at the end
of the long option (:argument
) results in an argument with a value instead
of a boolean switch. Using --value=default
results in a default value.
Because switches rarely need special behavior, the compact form is more common.
(transient-define-prefix transient-toys-wave ()
"Wave at the user"
["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)
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)
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.
; there is some built-in class introspection you might want to play with
; (eieio-help-class transient-infix)
(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 equavalent 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 ()
"Select and eat a snowcone"
:value '("--orange-snowcone") ; default value works!
["Arguments"
(transient-toys--snowcone-flavor)])
(transient-toys-snowcone-eater)
If you need to prevent two switches from being set simultaneously, you can
set the prefix property :incompatible
and a list of the long-style
switches you want to make exclusive.
Use a list of lists, where each sublist is the long argument style. Match
the use of =
in both arguments and switches.
Note: Currently due to behavior outlined in 155, mutual exclusion breaks if groups overlap. Workarounds are likely to contain corner case bugs.
(transient-define-prefix transient-toys-set-switches ()
:incompatible '(
;; uncommenting will break the other two due to this bug
; ("--switch" "--value=") ; mutual exclusion on pairs works fine
("--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)
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-l) to open the levels UI for the user. Press (C-l) again to change the active level. Press a key such as “we” to change the level for a child. After you cancel 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)
So far we have focused on UI behaviors, but none of the inputs are being used
in commands. 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
.
;; 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 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)