Skip to content

Developer Quick Start Guide

Psionic K edited this page Sep 22, 2021 · 30 revisions

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.

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

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

Infix

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:

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

API Examples

Hello Transient!

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

Binding to 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)

Property API

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

Transient Suffix API

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)

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

Adding More Groups

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.

Groups one on top of the other:

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

Groups side by side

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

Group on top of groups side by side

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

Group Labels

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)

Dynamic Labels

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)

Infix

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 for infixes 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.

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

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 [[https://magit.vc/manual/transient/Suffix-Slots.html#Slots-of-transient_002dinfix][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

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" "perigrine" "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" "perigrine" "otter"))])

(transient-toys-animals)

Choices from a function

(defun transient-toys--animal-choices (a b c)
 (if (eq 0 (random 2))
     '("fox" "kitten" "otter")
   '("ant" "perigrine" "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)
Clone this wiki locally