Skip to content

Developer Quick Start Guide

hokomo edited this page Sep 13, 2022 · 30 revisions

Transient Developer Quick Start

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.

Running Examples in Org Mode

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

Terminology

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.

Nesting Sub-Prefixes

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

Infix

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

Summary

  • 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

Objects & EIEIO

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)

Hello Transient!

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

Equivalent Forms

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 Compact Form

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)

Property List Style

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

Macro Child Definition Style

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)

Overriding in the Compact Form

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

Quoting Note for Vectors

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.

Groups & Layouts

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.

Layout

The default behavior treats groups a little differently depending on how they are nested. For most simple groupings, this is sufficient control.

Groups one on top of the other

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

Groups side by side

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

Group on top of groups side by side

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

Descriptions

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)

Dynamic Labels

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)

Infix - Setting Up Arguments

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 & Arguments

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)

Default Values

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)

Short Args

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.

Argument and Switch Macros

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

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)

Choices compact form

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)

Choices from a function

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

Mutually Exclusive Switches

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)

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

Prompt

Custom Readers

a reader on an infix

The reader definition

The function signatures are really similar to other completing reads. Check out programmed completions

Custom Scopes

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

The scope function

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!

Prefix Scope

Child Scope

Lisp Variables

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)

Custom Infix Types

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)

Rewrite a bit to store LOC for consumption via transient-current-prefixes

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 Key

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.

Prefix History

Magit defining some classes of prefixes with a single shared history key

Suffix History

Magit defining history key for an infix

I have no idea where corresponding behavior can be found.

Reading Arguments into Commands

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!

Simple Suffix Dispatch

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)

Reading Arguments from Other Commands

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

Dispatching args into a process

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 use transient--do-replace and transient-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)

Flow Control

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.

Pre-Commands

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)

Early completion

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

Errata

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)

Controlling Visibility

At times, you need a prefix to show or hide certain options depending on the context.

Predicates

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

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)

Show & Hide with transient-setup-children

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.

Clone this wiki locally