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.

Switches & Arguments

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)

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)

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

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