UP | HOME

Emacs Transient menu

Table of Contents

logo.jpg

Introduction

Are you still struggling with shortcuts and all the packages? or maybe you have created your own package and want a nice menu with flags and arguments. Well, Transient is for you! For some reason it took me quite some time to figure out how to use it. I don't want say I'm an expert, but maybe this can get you started at least. And for me, well this is a learning session. Trying to explain something that I'm not completely comprehend is a challenge , but the reward is that I will hopefully learn something through the journey. According to Ralph Waldo Emerson the American philosopher:

Its not the destination, its the journey

So lets start our journey.

History

So, what is Transient? Transient was originally a library used to implement keyboard-driven menus in Magit. It is distributed as a separate package, allowing other packages to implement similar menus.

Since I have used magit quite a bit , I was intrigued by the easiness and the user friendliness of the menu system. But to be fair, first time I took a look at it , I gave up, I thought I lacked emacs-lisp knowledge, found the terminology confusing, and found the entire experience quite intimidating. Maybe that story is more about me than of the documentation, but it ended up on the shelf of things I want to learn but never have time to.

Anyway, time passed, and for one reason or the other, I really needed a menu system which had the opportunity with flags and arguments. So, I started digging, and eventually I manage to create something.

But lets start from the beginning.

Once Upon a Time…

I won’t be retracing that journey myself. Instead, if you’re struggling, I highly recommend exploring the Transient showcase.

Standing on the shoulders of giants. (nani gigantum humeris insidentes)

I won’t cover the installation process here—that’s for you to handle. Instead, I’ll dive straight into discovering and exploring the capabilities of this package. Some sections may be more detailed than others, likely reflecting my own ignorance. But first, let’s clarify some of the key terminology.

prefix

prefix A prefix is a command that initiates a transient, such as displaying a menu. When you invoke a prefix command like M-x magit-dispatch, it opens a transient menu (also called a popup or transient popup).

2025-05-16_16-55-16_screenshot.png

That is; prefix is a generated function, when invoked, activates a temporary key map and displays the available command options.

Lets try this; first, we need to call: transient-define-prefix which is a macro and the documentation can be found here.

So lets do our first test.

(transient-define-prefix col/prefix-test ()
"Prefix that is minimal and uses an anonymous command suffix."
[("a" "Call me"
  (lambda ()
    (interactive)
    (message "Called a suffix")))
 ("b" "Or me"
      (lambda ()
        (interactive)
        (message "Called another suffix")))
 ])
(col/prefix-test)

When we invoke M-x col/prefix-test, we get:

2025-05-16_17-13-41_screenshot.png

This creates our first transient menu. It's basic, but functional. We've defined a prefix col/prefix-test which is an interactive function that displays a transient key map with suffix commands.

A complete transient consists of a prefix command and at least one suffix command, though typically a transient has multiple infix and suffix commands. The transient-define-prefix macro defines both the prefix command and binds all its associated suffix commands.

Since its necessary to understand the concept of a suffix I'll shortly explain it, then we can take a more closer look at it in later sections

A suffix is essentially an interactive function mapping that associates a key with corresponding documentation and a function. A short example would probably be good:

(defun col/my-suffix-fn ()
  "My interactive suffix fn"
  (interactive)
  (message "Hello suffix")
  )

(transient-define-prefix col/short-intro-prefix ()
  "Short introduction to a Suffix"
  ["Heading"
   ("a" "Pressing \"a\" runs Suffix" col/my-suffix-fn)
    ])

The above example maps

key
the first in the list is the keyword, in this case we use a
Documentation
which will output to explain what the key does.
Action
This is where we call the an interactive function

These three items grouped together is what is called a suffix. Now, there is a lot more to suffixes. But that is the basic idea. So anywhere where suffix is mentioned its actually referring to these 3 basic items grouped together. Lets not get ahead of ourselves prefix first.

Lets make a yas-snippet for the transient

(transient-define-prefix ${1:prefix-name} ()
  "${2:Documentation}"
  ${3:$$(yas-choose-value '(" " "<infix-inline" "<suffix-inline" "<group"))}$0
  )

Expanding this would create a basic transient prefix code, by calling the macro transient-define-prefix which will then expand to create a new interactive function called whatever the name of the prefix is.

Lets again make an small example:

(transient-define-prefix my-test-prefix ()
  "This is just for demonstration"
   )

We can now invoke M-x my-test-prefix, which will display a small menu at the bottom. However, since no keys or groups are defined, the menu is essentially empty and not very useful. This menu needs some information groups,/suffixes/ and/or infixes. But there are a couple of more features I like to present when defining a prefix.

If we look to the documentation of transient-define-prefix:

Macro: transient-define-prefix name arglist [docstring] [keyword value]… group… [body…]

We have covered some of it like name, group and docstring which is pretty straight forward. What I haven't covered yet is the arglist for example. Its possible to have arguments to the transient-define-prefix macro.

Transient prefix arguments

Let's imagine we want to select a file before opening the menu to provide some context. Let's reflect on what the official documentation states.

If BODY is specified, then it must begin with an interactive form that matches ARGLIST, and it must call transient-setup. It may, however, call that function only when some condition is satisfied.

An example here would be clarify more. For this example to work, we need something called a suffix, which has been covered very briefly. Lets first have a short overview to get this example going; We start with our transient-define-prefix which now takes an argument file ,but for this to work we also need a [BODY], that is some code that will run before the actual mini-buffer menu will open. The code initially specifies that this is an interactive function, prompting the user to select a file that will serve as the argument. The next step is to use transient-setup. If we continue looking at the documentation:

transient-setup function is called by transient prefix commands to setup the transient. In that case NAME is mandatory, LAYOUT and EDIT must be nil

lets focus on the easy stuff first: LAYOUT and EDIT must be nil.✓

Name is mandatory, that means we need a name, but what name? For this we need to look a bit further in the documentation. Here we find some more information

all transients have a (possibly nil) value, which is exported when suffix commands are called, so that they can consume that value.

The NAME actually refers to the name of the prefix.

so for example:

 1: (transient-define-prefix my/prefix-test-body ()
 2:   "A prefix with body"
 3:   ["Heading"
 4:    ("o" "Open file" (lambda ()                                       ;
 5:                       (interactive)
 6:                       (message "Transient with body")
 7:                       ))
 8:    ]
 9:   (interactive)                                                      ;
10:   (transient-setup 'my/prefix-test-body)
11:   )
Line 4
A suffix with a dummy message
Line 9
A BODY of the prefix, in this case it could be omitted since we dont have a arglist

The BODY is optional. If it is omitted, then ARGLIST is ignored and the function definition becomes:

(lambda ()
  (interactive)
  (transient-setup 'NAME))

If BODY is specified, then it must begin with an interactive form that matches ARGLIST, and it must call transient-setup.

The body must reflect what ever the arglist is, in our previous example, no arglist was used. Lets add a file as argument.

 1: (transient-define-prefix my/transient-file (file)
 2:   "Using file argument  for transient-define-prefix"
 3:   ["File operations"
 4:    ("o" "Open file" (lambda ()
 5:                       (interactive)
 6:                       (message "Transient with body %s %s" my-banan my-file )
 7:                       ))
 8:    ]
 9:   (interactive "fSelect file: ")
10:   (setq my-file file)
11:   (setq my-banan "Bananas 🍌 ooo")
12:   (transient-setup 'my/transient-file)
13:   )
  • Line 1 - we added a file argument, and as stated it needs begin with an interactive form that matches ARGLIST.
  • Line 9 - This reflects the interactive based on the argument.
  • Line 10 - Make a global my-file using file
  • Line 6 - uses the global variables my-file and my-banan

Even though this works as a solution, I would not recommend it. Global variables can make code and context quite messy, but at this point we dont have any other solution or tools. If we'd rather avoid dealing with global variables, how can we pass the file to the suffix Open file? Somehow we need to add the file to the "scope" of the suffix. Obviously there are better ways of achieve this than with global variables . Another way would be to use :scope to the transient-setup

Lets make this example somewhat more readable.

 1: (transient-define-prefix my/transient-file (file)
 2:   "Using file argument  for transient-define-prefix"
 3:   ["File operations"
 4:    ("o" "Open file" (lambda ()
 5:                       (interactive)
 6:                       (let* ((scope (transient-scope)))               ;
 7:                         (message "Transient with body %s %s" scope col/global-variable)
 8:                         )))
 9:    ]
10:   (interactive "fSelect file: ")
11:   (setq col/global-variable file)                                  ;
12:   (transient-setup 'my/transient-file nil nil :scope file)         ;
13:   )
  • Line 6 - we define a suffix; using the transient-scope we get the value from the scope.
  • Line 11 - Using setq will make the variable col/global-variable global, which is probably not what we intended.
  • Line 12 - By calling transient-setup with param :scope file we push the file to the scope, which then can be retrieved from the suffix.

Considering the new aspect of prefix, lets define a new yas-snippet for prefix.

(transient-define-prefix ${1:prefix-name} (${2:args})
  "${3:Documentation}"
  ${4:$$(yas-choose-value '(" " "<infix-inline" "<suffix-inline" "<group"))}$0

  ${5:(interactive ${6:"fSelect file:"})
  (transient-setup '$1 nil nil :scope $2)}
  )

Its not perfect, but it helps somewhat to get the right things into right place.

Groups

Groups are way to handle layout and headings, and are delimited by [....] If you begin a group vector with a string, you get a group heading.

(transient-define-prefix my-test-prefix ()
  "Documentation"
  [" Heading"
   ("a" "Doc suffix-fun1" (lambda () (interactive) (message "fun1")))
   ("b" "Doc Suffix-fun2" (lambda () (interactive) (message "fun2")))
   ])

Running this will create a menu shown as follows M-x my-test-prefix

groups_1.png

Where we can see the title: "heading" above the group that we created, which is horizontally aligned.

If we provide a additional vector, we would get a heading for the complete popup.

Lets make one more example

(transient-define-prefix my-test-prefix ()
  "Documentation"
  ["A TITLE\n\n"
   [" Heading"
    ("a" "Doc suffix-fun1" (lambda () (interactive) (message "fun1")))
    ("b" "Doc Suffix-fun2" (lambda () (interactive) (message "fun2")))
    ]
  ])

For each row we can create another group with another vector, this is so called stacked groups, which means they are stacked on top of each other (horizontally aligned groups)

(transient-define-prefix my-test-prefix ()
  "Documentation"
   [" Heading"
    ("a" "Doc suffix-fun1" (lambda () (interactive) (message "fun1")))
    ("b" "Doc Suffix-fun2" (lambda () (interactive) (message "fun2")))
    ]
   ["Stacked ontop"
    ("c" "Doc suffix-fun1" (lambda () (interactive) (message "funC")))
    ("d" "Doc Suffix-fun2" (lambda () (interactive) (message "funD")))
    ]
  )

2025-05-30_22-09-39_screenshot.png

If we want both a title and stacked

(transient-define-prefix my-test-prefix ()
  "Documentation"
  ["Title"
   [" Heading"
    ("a" "Doc suffix-fun1" (lambda () (interactive) (message "fun1")))
    ("b" "Doc Suffix-fun2" (lambda () (interactive) (message "fun2")))
    ]]
  ["Another Title (below)"
  ["Stacked ontop2"
    ("e" "Doc suffix-fun1" (lambda () (interactive) (message "fune")))
    ("f" "Doc Suffix-fun2" (lambda () (interactive) (message "funf")))
    ]]
  )

One thing to note is that the TITLE vector ends where after the first inlined vector (fun1, fun2). The reason for this is that if we have a vector with another vector of list. It will create a new column.

Lets make this more visible with an example:

(transient-define-prefix my-test-prefix ()
  "Documentation"
  ["Title"
   ["Row 1 col 1"
    ("a" "Doc suffix-fun1" (lambda () (interactive) (message "fun1")))
    ("b" "Doc Suffix-fun2" (lambda () (interactive) (message "fun2")))
    ]
  ["Row 1 col 2"
    ("e" "Doc suffix-fun1" (lambda () (interactive) (message "fune")))
    ("f" "Doc Suffix-fun2" (lambda () (interactive) (message "funf")))
    ]]
  )

2025-05-30_22-20-45_screenshot.png

I wrapped the two vectors inside another vector, and voilà, this created a new column with a new heading.

Lets imagine that we want to create a menu as follows

  • one title
  • One heading 1 with a,b,c mapped functions
  • one heading 2 below with c,d,e mapped functions
  • One heading right of heading 2 , f,g,h mapped function
(transient-define-prefix simple-menu-1 ()
  "Simple menu1"
  ["Title"
   ["Heading 1"
    ("a" "a function" (lambda () (interactive) (message "Function A")))
    ("b" "b function" (lambda () (interactive) (message "Function B")))
    ("c" "c function" (lambda () (interactive) (message "Function C")))
    ]]
  ["Below"
   ["Heading 2"
    ("d" "d function" (lambda () (interactive) (message "Function D")))
    ("e" "e function" (lambda () (interactive) (message "Function E")))
    ("f" "f function" (lambda () (interactive) (message "Function F")))
    ]
   ["Heading 3"
    ("g" "g function" (lambda () (interactive) (message "Function G")))
    ("h" "h function" (lambda () (interactive) (message "Function H")))
    ("i" "i function" (lambda () (interactive) (message "Function I")))
    ]
   ]
  )

2025-05-30_22-31-54_screenshot.png

This could be further extended , but I leave it up to the reader to play with. There are more to groups though; In the example above I used static strings, but these could actually be functions providing e.g HEADING instead. This time we will use property :description for the group to insert a dynamic name.

Info

Info is another feature that can be used to add some information into the actual menu. In the below example i added banans

(transient-define-prefix my-test-dyn-prefix ()
  "Documentation"
  [:description (lambda () (format-time-string "%H:%M:%S"))
   [:description current-time-string :pad-keys t
    ("a" "Doc suffix-fun1" (lambda () (interactive) (message "fun1")))
    (:info "Bananas 🍌🍌")
    ("banan" "Doc Suffix-fun2" (lambda () (interactive) (message "fun2")))
    ]
   ["Test"
    ""
    ("c" "Doc Suffix-fun2" (lambda () (interactive) (message "fun2")))
    ""
    ("d" "Doc Suffix-fun2" (lambda () (interactive) (message "fun2")))
    ]
   ])

On the other hand, an empty vector represented as `[[]]` does nothing, and including an empty line in a vector has no effect. A empty group ["group"] will be ignored. Though if in a group you add a empty string, it acts as a line seprator for that column.

2025-06-04_17-09-44_screenshot.png

If for some reason you end up with really long names you might need to use :pad-keys t, this aligns the menus.

Group specifications

Overriding class

Its possible to override the class by specifying :class

(transient-define-prefix override-transient-class ()
  "Overrinding the transient class in group"
  [ :class transient-row
    ("a" "←" (lambda () (interactive) (message "Left")))
    ("w" "↑" (lambda () (interactive) (message "Up")))
    ("d" "→" (lambda () (interactive) (message "Right")))
    ]
  [ :class transient-row
    ""
    ""
    ("s" "↓" (lambda () (interactive) (message "Down")))
    ]
  [ :class transient-column
    ("SPC" "🤯" (lambda () (interactive) (message "explode")))
    (:info "Compass")
    (:info "Using w,a,s,d")
    ]
  )

In other words, you can use the class keyword to define the grouping layout, allowing for more precise and customizable arrangements.

Group_override_class.png

transient-columns

In the previous example we displayed the layout be defining rows and columns. The example did not get exactly make a compass as we wanted. Another approach is to use a more abstract grouping mechanism called :class transient-columns. This group vector contains other groups that are organized into columns rather than arranged in a row layout.

A small minimal example would suffice to start with:

(transient-define-prefix my-transient-columns ()
  "Transient-subgroups example."
  [:class transient-columns
    ["C1"
     ("r1" "Execute A" (lambda () (interactive) (message "R1")))
     ("r2" "Execute A" (lambda () (interactive) (message "R2")))]
    ["C2"
     ("r3" "Execute A" (lambda () (interactive) (message "R3")))
     ("r4" "Execute A" (lambda () (interactive) (message "R4")))]

    ["C3"
     ("r5" "Execute A" (lambda () (interactive) (message "R5")))
     ("r6" "Execute A" (lambda () (interactive) (message "R6")))]
   ])

Each subgroup represent a column. This basically switches over the default behaviour that each subgroup is a row to become a column instead.

transient-columns.png

Lets refine the compass example

(transient-define-prefix override-transient-class ()
"Overrinding the transient class in group"

[:class transient-columns
[ :class transient-row "Left"
  ("a" "←" (lambda () (interactive) (message "Left")))

  ]
[:class transient-row "Horizontal"
        ("w" "↑" (lambda () (interactive) (message "Up")))
        ("s" "↓" (lambda () (interactive) (message "Right")))
 ]
[:class transient-row "Right"
  ("d" "→" (lambda () (interactive) (message "Right")))
 ]

[:class transient-row "Fire"
  ("SPC" "🔥" (lambda () (interactive) (message "Fire")))
 ]
])

I could have skipped the :class transient-row since it’s always a row, but I included it for clarity.

2025-07-31_19-09-20_screenshot.png

transient-subgroup

Sometimes it’s useful to organize groups into subgroups, allowing you to manage each group independently from each other. Fortunatly there is group class called transient-subgroup. Each subgroup are reponsible for displaying their element, an empty line will be inserted between the subgroups.

The following example will make use of subgroups, though to make this clearer I used variables to make the subgroup.

 1: (setq compass-group   [:class transient-columns
 2:                      [ :class transient-row "Left"
 3:                        ("a" "←" (lambda () (interactive) (message "Left")))]
 4:                      [:class transient-row "Horizontal"
 5:                              ("w" "↑" (lambda () (interactive) (message "Up")))
 6:                              ("s" "↓" (lambda () (interactive) (message "Right")))]
 7:                      [:class transient-row "Right"
 8:                              ("d" "→" (lambda () (interactive) (message "Right")))]
 9:                      [:class transient-row "Fire"
10:                              ("SPC" "🔥" (lambda () (interactive) (message "Fire")))
11:                              ]])
12: 
13: 
14: (transient-define-prefix test-transient-subgroups ()
15:   "Testing transient subgroups"
16:   [:class transient-subgroups
17:           [:class transient-row
18:              ("q" "↖" (lambda () (interactive) (message "West Up")))
19:              (:info "Di. Up")
20:              ("e" "↗" (lambda () (interactive) (message "East Up")))
21:           ]
22:           compass-group
23:           [:class transient-row
24:              ("z" "↙" (lambda () (interactive) (message "West Down")))
25:              (:info "Di. Dn")
26:              ("x" "↘" (lambda () (interactive) (message "East Down")))
27:           ]])

compass_dir.png

The above example we have 3 different subgroups each representing its own grouping.

Line 16
Group defining a group with diagonal up directons
Line 22
Defines the compass, which is set in variable compass-group 1
Line 23
Group defining the diagonal down arrows

As mentioned earlier, each group is independent in layout from the others.

Suffixes

Suffixes has been briefly metioned in Prefixes, Now its time to dig slightly deeper into how suffixes works and are defined. In the previous mentiones, we noted that a suffix is a list of key, documentation and some kind of action , where the action is implemented as a interactive function. We already seen a few simple examples, though not very useful. Lets continue this road with another not so very useful example.

(defun first-fun ()
  "Message"
  (interactive)
  (message "Hello first-fun"))


(defface my-custom-face
'((t (:foreground "Red" :weight bold)))
"A custom face with deep pink bold text.")

(transient-define-prefix suffix-tutorial-one ()
  "calle test function"
  ["Heading"
   ("a" "Document" first-fun :transient t :summary "Hello")
   ("q" "Quit" transient-quit-one :face 'my-custom-face)
   ])

The only difference here is that we add :transient t and :face my-custom-face, this is called a Keyword or SLOT and its usefulness is to set some property which makes the suffix behave or look in a certain manner. The :transient t option prevents the transient menu from closing after executing the suffix action. Another is :face which will set the properties of the text for the suffix. In this example a custom face was created and used in the quit suffix.

There are multiple distinct SLOTS, but before discussing the different slots lets dig into another macro transient-define-suffix

transient-define-suffix macro

Earlier, we defined a suffix as a combination of a key, Documentation, and an action within a list. That holds true for the simplest form of a transient, we also seen that there are slots available that can modify properties for a suffix. To facilitate the creation of suffixes, the transient package created a macro transient-define-suffix, similair to prefix.

The best way to show this is by example.


(transient-define-suffix my-user-suffix ()
  "This is my suffix"
  (interactive)
  (message "User name: %s" (getenv "USER")))

(transient-define-suffix my-home-suffix ()
  "This is my suffix"
  (interactive)
  (message "Home Dir: %s" (getenv "HOME")))

(transient-define-prefix my-transient-prefix ()
  "Prefix "
  ["User"
   ("u" "User" my-user-suffix)
   ("h" "Home Dir" my-home-suffix)]
  )

This looks very similair to a defun. Every suffix needs a NAME,

The BODY must begin with an ‘interactive’ form that matches ARGLIST.

In this example, we did not use an arglist, as prefix commands allow adding arguments directly. We define a new suffix, my-buffer-suffix, which accepts a buffer argument and inherits from the transient-suffix class.

(transient-define-suffix my-buffer-suffix (buffer)
  "A suffix that uses a buffer argument."
  :class transient-suffix
  (interactive "bSelect buffer:")
  (message "Buffer name: %s" buffer))

(transient-define-suffix my-user-suffix ()
  "A suffix that retrieves the USER environment variable."
  (interactive)
  (message "User name: %s" (getenv "USER")))

(transient-define-suffix my-home-suffix ()
  "A suffix that retrieves the HOME environment variable."
  (interactive)
  (message "Home directory: %s" (getenv "HOME")))

(transient-define-prefix my-transient-prefix ()
  "A prefix demonstrating user, home, and buffer suffixes."
  ["User"
   ("u" "User" my-user-suffix)
   ("h" "Home" my-home-suffix)
   ("b" "Buffer" my-buffer-suffix)
   ])

Wouldnt it be great if we could define the key, and documentation in the transient-define-suffix instead, obviously that can be done. By using properties we can change some of the feature of the suffix.

(defface my-custom-face
  '((t (:foreground "Red" :weight bold)))
  "A custom face with deep pink bold text.")


(transient-define-suffix my-suffix-test ()
  "docstring"
  :key "T"
  :description "press T"
  (interactive)
  (message "%s" "Pressed T"))

(transient-define-suffix my-select-buffer (buffer)
  "A suffix that uses a buffer argument."
  :class transient-suffix
  :transient t
  :key "b"
  :description "Select a buffer"
  (interactive "bSelect buffer:")
  (message "Buffer name: %s" buffer))


(transient-define-suffix my-user-suffix ()
  "A suffix that retrieves the USER environment variable."
  :key "u"
  :face 'my-custom-face
  :description "Show current user."
  (interactive)
  (message "User name: %s" (getenv "USER")))

(transient-define-suffix my-home-suffix ()
  "A suffix that retrieves the HOME environment variable."
  :key "h"
  :description "Show current home directory"
  (interactive)
  (message "Home directory: %s" (getenv "HOME")))

(transient-define-prefix my-transient-prefix ()
"A prefix demonstrating user, home, and buffer suffixes."
["Useless"
 ["User vals"
 (my-suffix-test)
 (my-home-suffix)
 (my-user-suffix)
 ]
 ["Buffer"
 (my-select-buffer)
 ]]
)

We all seen the suffixes before, but in another shape. This time we used the transient-define-suffix and specified the keywords, documentation and action in the macro together with some other keywords (face,transient …..). There are many other keywords that can be used , for example to apt out suffixes that are should not be available for some reason. But we leave that to a later time for now. First we need to cover the infixes.

So far, the prefix creates the main menu and suffixes execute actions. The missing piece is a way to set and unset values. This is the role of infixes. But lets have a short look at the keywords for suffixes (C-h f transient-suffix)

Keywords

Lets look into different suffix keywords

Name Type Default Description
parent nil nil The parent group object.
level nil nil Enable if level of prefix is equal or greater.
if nil nil Enable if predicate returns non-nil.
if-not nil nil Enable if predicate returns nil.
if-non-nil nil nil Enable if variable’s value is non-nil.
if-nil nil nil Enable if variable’s value is nil.
if-mode nil nil Enable if major-mode matches value.
if-not-mode nil nil Enable if major-mode does not match value.
if-derived nil nil Enable if major-mode derives from value.
if-not-derived nil nil Enable if major-mode does not derive from value.
inapt nil nil  
inapt-face symbol 'transient-inapt-suffix Face used when inapt.
inapt-if nil nil Inapt if predicate returns non-nil.
inapt-if-not nil nil Inapt if predicate returns nil.
inapt-if-non-nil nil nil Inapt if variable’s value is non-nil.
inapt-if-nil nil nil Inapt if variable’s value is nil.
inapt-if-mode nil nil Inapt if major-mode matches value.
inapt-if-not-mode nil nil Inapt if major-mode does not match value.
inapt-if-derived nil nil Inapt if major-mode derives from value.
inapt-if-not-derived nil nil Inapt if major-mode does not derive from value.
advice nil nil Advise applied to the command body.
advice* nil nil Advise applied to the command body and interactive spec.
key symbol 'eieio–unbound  
command symbol 'eieio–unbound  
transient symbol 'eieio–unbound  
format string " %k %d"  
description nil nil  
face nil nil  
show-help nil nil  
summary nil nil  

Obviously as one can see there are much more to this than I have covered, maybe I will continue this with more in depth post someday. But for now i'll leave it at this.

Suffix yas-snippet

Since writing all this code can be time-consuming and tedious, I often rely on shortcuts. For suffixes, I typically use a yas-snippet. While it doesn't cover every possible KEYWORD, it provides a solid starting point.

# -*- mode: snippet -*-
# name: Transient suffix
# key: <suffix
# group: col
# --
(transient-define-suffix ${1:suffix-name}(${2:args})
  "${3:Documentation string}"
  ${4::description "${5:Description}"}
  ${6::key "${7:A}"}                  ;; Key to trigger this suffix
  ${8:(interactive ${9:"fFile:"})
      (let* ((args (transient-args '${10:prefix}))
             (value (transient-arg-value "${11:--option=}" args))
            )
        ${12:(message "Value %s" value)}
        ))}
$0

Some parts of this code haven't been explained yet (transient-args, transient-arg-value), but they will be covered in later sections (see Reading infix in suffix).

Infix

In Emacs Transient, infix arguments allow users to specify additional options or modify the behavior of commands before executing them. They act as temporary settings that influence how the subsequent command operates.

With infix, you can:

  • Toggle flags that alter command functionality.
  • Input numeric arguments or parameters.
  • Enable or disable specific modes within the transient menu.
  • Chain multiple modifications to customize command execution.

Essentially, infix keys let you fine-tune commands interactively within the transient interface before confirming the action (suffix).

In essence, a infix is a specialized type of suffix, but instead of executing a function, it stores values. If we look at the M-x (eieio-browse), we can see that the transient-infix class is actually a descendant of the transient-suffix class, meaning it inherits all the properties of a suffix.


|    +--transient-suffix
|         +--magit--git-submodule-suffix
|         +--transient-describe-target
|         +--transient-value-preset
|         +--transient-infix

Anyway, lets not dwelve more on this , lets take a look of how we can use it. Lets start with something really simple, a flag.

Infix-basic

As with suffixes it the eassiest is as a list in the prefix for example.


(transient-define-prefix my-first-infix()
  ["Options"
   ("-s" "switch" "--switch")
   ("-f" "user" "--user=" )
  ]
  )
first element
Key to use
Second element
Description
Third element
argument , the actual option as passed to the underlying command.

The only difference between a suffix and a infix is the third element, which in suffix was an action, but with infix it becomes an argument , which can later be retrieved.

my_first_infix.png

If we press -s the --switch will toggle and become set. If we press -f we will get a prompt --user=┃ where you can enter a value which will be assigned. For example if I both set the switch and add a user. it will look like

my_first_infix_set.png

The reason one is a switch and the other is a value depends on the =-sign at the end of the third argument. When the third arguments looks like --user= it will automatically prompt the user for a input. As with suffixes there is also a macro which makes the code much more readable

transient-define-infix macro

Following the previous standard (prefix,suffix) the macro is defined as transient-define-infix. This makes the code much more readable, especially if we are adding more features to the infix. As with suffixes there are keywords that can be used.

Name Default Description
parent nil The parent group object.
level nil Enable if level of prefix is equal or greater.
if nil Enable if predicate returns non-nil.
if-not nil Enable if predicate returns nil.
if-non-nil nil Enable if variable’s value is non-nil.
if-nil nil Enable if variable’s value is nil.
if-mode nil Enable if major-mode matches value.
if-not-mode nil Enable if major-mode does not match value.
if-derived nil Enable if major-mode derives from value.
if-not-derived nil Enable if major-mode does not derive from value.
inapt nil Marks suffix as inapt (unusable) under conditions.
inapt-face 'transient-inapt-suffix Face used to display inapt suffixes.
inapt-if nil Inapt if predicate returns non-nil.
inapt-if-not nil Inapt if predicate returns nil.
inapt-if-non-nil nil Inapt if variable’s value is non-nil.
inapt-if-nil nil Inapt if variable’s value is nil.
inapt-if-mode nil Inapt if major-mode matches value.
inapt-if-not-mode nil Inapt if major-mode does not match value.
inapt-if-derived nil Inapt if major-mode derives from value.
inapt-if-not-derived nil Inapt if major-mode does not derive from value.
advice nil Advice applied to the command body.
advice* nil Advice applied to the command body and interactive spec.
key 'eieio–unbound Keybinding for the suffix command (initially unbound).
command 'eieio–unbound The command executed by this suffix (initially unbound).
transient t Flag indicating this is a transient command.
format " %k %d (%v)" Format string used to display the suffix entry.
description nil Description string or function describing the suffix.
face nil Face used to display the suffix.
show-help nil Function or flag to show help for the suffix.
summary nil Summary text for the suffix.
argument 'eieio–unbound Argument passed to the command (initially unbound).
shortarg 'eieio–unbound Short argument representation (initially unbound).
value nil Current value of the infix or suffix.
init-value 'eieio–unbound Initial value or function providing it (unbound by default).
unsavable nil If non-nil, value is not saved in history.
multi-value nil If non-nil, multiple values are permitted.
always-read nil If non-nil, always prompt for input, even if value exists.
allow-empty nil If non-nil, empty input is allowed.
history-key nil Key used for saving input history.
reader nil Reading function used to parse input.
prompt nil Prompt string or function used when reading input.
choices nil Allowed choices for the value (if any).

Lets try it out.

 1: 
 2: (transient-define-infix option-read-file ()
 3:   "Using transient-define-infix."
 4:   :class 'transient-option (one-trans-opt)
 5:   :description "Choose a file"
 6:   :argument "--file=" (one-trans-arg)
 7:   :shortarg "-f"
 8:   :reader 'read-string (one-trans-reader)
 9:   :prompt "Type in a file name: " (one-trans-prompt)
10:   )
11: 
12: (transient-define-prefix my-second-infix ()
13:   "My second infix with file option."
14:   [["Options"
15:     (option-read-file)]
16:   ])

Lets disect this code.

line one-trans-opt
Here we chose to use class option , this will provide us with a value prompt, this to be able to actually set a value instead of just a switch
line one-trans-arg
what arguments do we want to use , for passing on for later retrieval.
line one-trans-reader
What reader do we want , in this case we used a string reader, but imagine if we wanted some other kind of value. for example number, buffer , file-name…..
line one-trans-prompt
What prompt do we want to give to the user when entering a value?

Options limit

Sometimes you might want to limit the options, lets say you only want the user to define an option from a list (like a drop down list), and you always want to use one option as default.

 1: (defface my-custom-face
 2:   '((t (:foreground "Red" :weight bold)))
 3:   "A custom face with deep pink bold text.")
 4: 
 5: 
 6: (transient-define-infix my-selection-infix ()
 7:   "Only select one of the options"
 8:   :description "Choose protocol"
 9:   :class 'transient-option
10:   :argument "--protocol="
11:   :shortarg "-p"
12:   :face 'my-custom-face
13:   :prompt "Choose protocol: "
14:   :choices '("http" "https" "ftp")                            ;
15:   :init-value (lambda (obj) (progn (oset obj value "https"))) ;
16:   :always-read t                                              ;
17:   )
18: 
19: 
20: 
21: (transient-define-prefix my-third-infix ()
22: "My third infix with file option."
23: [["Options"
24:   (my-selection-infix)]
25: ])
26: 

Lets break this down to further understand it.

Line 14
By using keyword :choices we can provide a list of different choices that can be made.
Line 15
The initial value is a function that takes an object as its argument; in this context, the lambda sets the default value of the option to "https" by modifying the object's value slot using the `oset` function. This ensures that when the transient is first invoked, "https" is preselected as the default choice.
Line 16
This forces the infix to always prompt the user for a value. If this is not set to true, it is possible to have unselected values.

The 15 might be hard to understand, so I'll try to explain it as good as i can. The object that is passed in as arguments is a eieio-object, which has some properties, or what they call SLOTS, one of these slots are named value. By using oset we can set this property/slot to a value which will be seen as a default value.

third-infix.png

Infix reader

The infix reader is a powerful customization within transient package: it lets you control how a value is read from the user when the activate an infix argument.

The :reader specifies a functio used to read the value for the infix argument So, for example if i want to read just a number, then i would need a number-reader-function.

Lets take a look how that function signature. It takes three arguments:

  1. Prompt :: The string shown in the minibuffer
  2. Initial-input :: what (if anything) to pre-fill as initial value in the prompt.
  3. History :: The minibuffer history variable to record/read previous input.

The function should return the value.

Lets make an example:

 1: 
 2: (transient-define-infix example-infix-number-reader ()
 3: "An example with a custom number-reader."
 4: :description "Example number reader"
 5: :class 'transient-option
 6: :key "v"
 7: :argument "--value="
 8: :reader (lambda (prompt _init _hist)
 9:             (number-to-string (read-number prompt))))
10: 
11: (transient-define-infix example-infix-file-reader()
12:   "An example with a custom file-reader"
13:   :description "Read file"
14:   :shortarg "-f" ;; Single-char short key
15:   :argument "--file" ;; Full flag
16:   :class 'transient-option ;; Either option (value) or switch (flag)
17:   :prompt "Add file"
18:   :always-read t
19:   :reader (lambda (prompt _initial-input _history)
20:               (read-file-name prompt)))
21: 
22: (transient-define-prefix example-prefix-reader ()
23:   "Dummy prefix"
24:   ["Options"
25:    (example-infix-number-reader)
26:    (example-infix-file-reader)
27:    ])
28: 

In the example above, use `=v=` to add a number and `=-f=` to add a file. Now, let's take a closer look at the readers:

Line 7
This only accepts number as input, anything else it it will display an error and the prompt will return. (see C-h f read-number), the return value is then transformed into a string , which will (if its a number) be displayed.
Line 18
File reader is using the read-file-name (see C-h f read-file-name) which returns a string , so no need for transformation.

The final output:

infix_reader_example.png

Obviosly you can build your own reader, where you only allow certain words, string or what ever. But I will leave it at this.

Reading infix in suffix

The suffix function needs to be able to retrieve the values that are defined using the infixes. This is the last part to a complete menu system. The idea is that we enable the menu system my calling the prefix. This takes care of showing the sub-menus and the layout. The infixes are then laid out to the user and , after user been setting options and switches, it would call a suffix to execute some function.

This is basically the workflow. But we are missing a key point in this structure. How do we get the infixes values to the suffix? A natural way would be to think it would come as an argument to the suffix. But that is not what happens, as with scope, argument are used for interactive things like choosing a buffer or file. So how do we retrieve the values?

the first thing we need to do is to retrieve the infix arguments, this is done through the function (transient-args PREFIX). This will return the active arguments of the PREFIX. To get the actual values we need to provide the args to transient-arg-value ARG ARGS

lets make a small example.

Lets create small option to set some string value:

1: (transient-define-infix my-infix-opt()
2:   "Infix option"
3:   :description "add some option"
4:   :shortarg "-o" ;; Single-char short key
5:   :argument "--option=" ;; Full flag
6:   :class 'transient-option ;; Either option (value) or switch (flag)
7:   :always-read t
8:   )

Lets also define a switch.

1: 
2: (transient-define-infix my-inifix-switch()
3:   "Infix switch"
4:   :description "Set or not set"
5:   :shortarg "-s" ;; Single-char short key
6:   :argument "--switch" ;; Full flag
7:   :class 'transient-switch ;; Either option (value) or switch (flag)
8:   )

We also need something a suffix to read out the values

 1: (transient-define-suffix my-suffix()
 2:   "My suffix"
 3:   :description "Executing and retrieving values"
 4:   :key "e"                  ;; What key to execute
 5:   (interactive )
 6:   (let* ((args (transient-args 'my-prefix-opts))
 7:          (value (transient-arg-value "--option=" args ))
 8:          (switch-val (transient-arg-value "--switch" args))
 9:          )
10:     (message "Value Opt: %s Switch: %s" value (if switch-val "Set" "Not set"))
11:     ))
12: 

Just a short dissect of the suffix code.

Line 6
To fetch the prefix arguments we need to call transient-args with the prefix function that invoked it, this may be a list if we have more prefix functions that we want to retrieve values from.
Line 7
From the args variable we want to retrieve the "–option=" this has to match the exact argument from the infix.
Line 8
The switch is either nil or t which we can use together with if.

And finally we want to make a prefix that organizes the infix and suffix.

 1: 
 2: (transient-define-infix my-inifix-switch()
 3:   "Infix switch"
 4:   :description "Set or not set"
 5:   :shortarg "-s" ;; Single-char short key
 6:   :argument "--switch" ;; Full flag
 7:   :class 'transient-switch ;; Either option (value) or switch (flag)
 8:   )
 9: 
10: (transient-define-infix my-infix-opt()
11:   "Infix option"
12:   :description "add some option"
13:   :shortarg "-o" ;; Single-char short key
14:   :argument "--option=" ;; Full flag
15:   :class 'transient-option ;; Either option (value) or switch (flag)
16:   :always-read t
17:   )
18: 
19: (transient-define-suffix my-suffix()
20:   "My suffix"
21:   :description "Executing and retrieving values"
22:   :key "e"                  ;; What key to execute
23:   (interactive )
24:   (let* ((args (transient-args 'my-prefix-opts)) (args)
25:          (value (transient-arg-value "--option=" args )) (value)
26:          (switch-val (transient-arg-value "--switch" args)) (switch)
27:          )
28:     (message "Value Opt: %s Switch: %s" value (if switch-val "Set" "Not set"))
29:     ))
30: 
31: 
32: 
33: (transient-define-prefix my-prefix-opts()
34:   "My prefix using switches and options"
35:   ["Exec"
36:    (my-suffix)
37:     ]
38:   ["Options"
39:    (my-inifix-switch)
40:    (my-infix-opt)
41:    ]
42: 
43:   )

The menu is simple:

menu_opts_reader.png

When we execute our suffix the mini-buffer will show:

Value Opt: test Switch: Set

Small project

So lets make a small project 😼. Lets look at org-kanban , creating a kanban board for a org file.

Initialize

There are three different initializations.

  • At the beginning
  • At the end
  • At the point.

So we can create our first inifix to be an option

(transient-define-infix kanban-initialize-options()
  "Either choose beginning,here or the end"
  :description "Where to initialize the board"
  :class 'transient-option
  :argument "--init-place="
  :shortarg "-i"
  :choices '("beginning" "here" "end")
  :init-value (lambda (obj) (oset obj value "end"))
  :always-read t)

Now we need a suffix to execute the initialization. The suffix, would need to retrieve the value from infix (--init-place=).

 1: (transient-define-suffix kanban-init-exec()
 2:   "initilize the kanbanboard"
 3:   :key "i"
 4:   :description "Execute initialization"
 5:   (interactive)
 6:   (let* ((args (transient-args 'kanban/prefix))
 7:          (where (transient-arg-value "--init-place=" args))
 8:          )
 9: 
10:     (pcase where
11:       ("here" (org-kanban/initialize-here))
12:       ((or "beg" "beginning") (org-kanban/initialize-at-beginning))
13:       ((or "end" "end") (org-kanban/initialize (point-max)))
14:       (_ (message "Unknown place: %s" where)))))

This is not perfect; right now the begnning is at the top, which is not suitable. I would rather place the board before the first heading. Lets try fix this.

(defun kanban/initialize-at-first-heading ()
  "initialize kanban board at the first heading"
  (save-excursion
  (goto-char (point-min))
  (when (org-next-visible-heading 1)
    (org-kanban/initialize)
    )))



(transient-define-suffix kanban/init-exec()
  "initilize the kanbanboard"
  :key "i"
  :description "Initialize board"
  (interactive)
  (let* ((args (transient-args 'kanban/prefix))
         (where (transient-arg-value "--init-place=" args))
         )

    (pcase where
      ("here" (org-kanban/initialize-here))
      ((or "beg" "beginning") (kanban/initialize-at-first-heading))
      ((or "end" "end") (org-kanban/initialize-at-end))
      (_ (message "Unknown place: %s" where)))))

Update kanban

The next function I want is to be able to update the kanban-board through a function. This is a bit more trickier, but this is how i think it should work,

  1. goto start of the buffer
  2. Search forward forward for the dynamic block kanban
    1. if not found return error message (no kanban board found)
  3. when its found execute the org-dblock-update
  4. Go to the next 2.

There is a function called (org-find-dblock) the problem is that this only finds the first dynamic-block of some name, but since we consider the fact that there might be more than one we need to search using regexp instead.

I'll start from the bottom, the point is now at the kanban board, we need to update it.

(defun kanban-search-forward-for-board (pt name fn)
  "search forward for the dynamic board "
  (let* ((board-regexp (format "^#\\+BEGIN: %s" name)))
    (goto-char pt)
    (when (re-search-forward board-regexp nil t)
      (goto-char (match-beginning 0))
      (funcall fn)
      (point))))
kanban-search-forward-for-board

Now this could be called for example M-: (kanban-search-forward-for-board (point) "kanban" #'org-dblock-update)" To find the next kanban board, and update it. This could also make out to do other stuff using some other function. It will return t if found and nil if not.

But lets continue; the next part is to try executing a function on all kanban blocks.

 1: (defun kanban-exec-fn-all-blocks (fn &optional pt name)
 2:   "Call fn for all the db block, if pt is not set then using point-min. if NAME is not set its kanban"
 3:   (let* ((start (or pt (point-min)))
 4:          (dblock-name (or name "kanban"))
 5:          (found-point (kanban-search-forward-for-board start dblock-name fn ))
 6:          )
 7:     (goto-char start)
 8:     (when found-point
 9:       (goto-char found-point)
10:       (forward-line 1)
11:       (kanban-search-forward-for-board fn (point) dblock-name)
12:       )
13:     )
14:   )
(defun kanban-exec-fn-all-blocks (fn &optional pt name)
"Call FN for all the db blocks, starting from PT (or point-min). If NAME is not set, use 'kanban'."
(let* ((start (or pt (point-min)))
       (dblock-name (or name "kanban"))
       (found-point (kanban-search-forward-for-board start dblock-name fn)))
  (if found-point
      (progn
        (goto-char found-point)
        (forward-line 1)
    ;; Recursive call: next search starts at (point)
        (1+ (kanban-exec-fn-all-blocks fn (point) dblock-name)))
    0)
  ))

Now this can be called with M-: (kanban-exec-fn-all-blocks #'org-dblock-update) and it will return the number of boards that was that was executed with fn

So we got ourselfs some good starting point. But we also need to create a suffix for this function

(transient-define-suffix kanban-update-boards()
  "Update all boards"
  :description "Update boards"
  :key "u"                  ;; What key to execute
  (interactive )
  (save-excursion
  (let* ((args (transient-args 'kanban-prefix))
         (value (transient-arg-value "--option=" args))
         )
    (kanban-exec-fn-all-blocks #'org-dblock-update)
    )))

Field editing

Another feature that would be nice is to be able to change the state in a kanban board which would reflect on the state of the heading.

This can be achived through using (org-kanban//move directon) Where direction could be either one of these

  • right
  • left
  • down
  • up

I would like to make these suffixes using the arrow keys.

(transient-define-suffix kanban-field-right()
  "Moving a field to the right"
  :description "→"
  :key "<right>"
  :format " %d"
  :transient t
  (interactive)
    (org-kanban//move 'right)
    )

(transient-define-suffix kanban-field-left()
  "Moving a field to the Left"
  :description "←"
  :key "<left>"
  :format " %d"
  :transient t
  (interactive)
    (org-kanban//move 'left)
    )

(transient-define-suffix kanban-field-up()
  "Moving a field to the Left"
  :description "↑"
  :key "<up>"
  :transient t
  :format " %d"
  (interactive)
    (org-kanban//move-subtree 'up)
    )

(transient-define-suffix kanban-field-down()
  "Moving a field to the Left"
  :description "↓"
  :key "<down>"
  :format " %d"
  :transient t
  (interactive)
    (org-kanban//move-subtree 'down)
    )

Moving between lines

Moving between table rows seems like a useful suffix, and pretty straight forward.

p
Previous row
n
next row

Obviously one can put in much more elaborate things here, but this suffice for now.

(transient-define-suffix kanban-row-up()
  "move one row up "
  :description "Prev row"
  :key "p"                  ;; What key to execute
  :transient t
  (interactive)
  (forward-line 1)
  )
(transient-define-suffix kanban-row-down()
  "move one row up "
  :description "Next row"
  :key "n"                  ;; What key to execute
  :transient t
  (interactive)
  (forward-line -1)
  )

Prefix

  1: 
  2: (transient-define-infix kanban-initialize-options()
  3:   "Either choose beginning,here or the end"
  4:   :description "Where to initialize the board"
  5:   :class 'transient-option
  6:   :argument "--init-place="
  7:   :shortarg "-i"
  8:   :choices '("beginning" "here" "end")
  9:   :init-value (lambda (obj) (oset obj value "end"))
 10:   :always-read t)
 11: 
 12: (defun kanban/initialize-at-first-heading ()
 13:   "initialize kanban board at the first heading"
 14:   (save-excursion
 15:   (goto-char (point-min))
 16:   (when (org-next-visible-heading 1)
 17:     (org-kanban/initialize)
 18:     )))
 19: 
 20: 
 21: 
 22: (transient-define-suffix kanban/init-exec()
 23:   "initilize the kanbanboard"
 24:   :key "i"
 25:   :description "Initialize board"
 26:   (interactive)
 27:   (let* ((args (transient-args 'kanban/prefix))
 28:          (where (transient-arg-value "--init-place=" args))
 29:          )
 30: 
 31:     (pcase where
 32:       ("here" (org-kanban/initialize-here))
 33:       ((or "beg" "beginning") (kanban/initialize-at-first-heading))
 34:       ((or "end" "end") (org-kanban/initialize-at-end))
 35:       (_ (message "Unknown place: %s" where)))))
 36: 
 37: 
 38: (transient-define-suffix kanban-update-boards()
 39:   "Update all boards"
 40:   :description "Update boards"
 41:   :key "u"                  ;; What key to execute
 42:   (interactive )
 43:   (save-excursion
 44:   (let* ((args (transient-args 'kanban-prefix))
 45:          (value (transient-arg-value "--option=" args))
 46:          )
 47:     (kanban-exec-fn-all-blocks #'org-dblock-update)
 48:     )))
 49: 
 50: 
 51: (transient-define-suffix kanban-field-right()
 52:   "Moving a field to the right"
 53:   :description "→"
 54:   :key "<right>"
 55:   :format " %d"
 56:   :transient t
 57:   (interactive)
 58:     (org-kanban//move 'right)
 59:     )
 60: 
 61: (transient-define-suffix kanban-field-left()
 62:   "Moving a field to the Left"
 63:   :description "←"
 64:   :key "<left>"
 65:   :format " %d"
 66:   :transient t
 67:   (interactive)
 68:     (org-kanban//move 'left)
 69:     )
 70: 
 71: (transient-define-suffix kanban-field-up()
 72:   "Moving a field to the Left"
 73:   :description "↑"
 74:   :key "<up>"
 75:   :transient t
 76:   :format " %d"
 77:   (interactive)
 78:     (org-kanban//move-subtree 'up)
 79:     )
 80: 
 81: (transient-define-suffix kanban-field-down()
 82:   "Moving a field to the Left"
 83:   :description "↓"
 84:   :key "<down>"
 85:   :format " %d"
 86:   :transient t
 87:   (interactive)
 88:     (org-kanban//move-subtree 'down)
 89:     )
 90: 
 91: 
 92: 
 93: (transient-define-prefix kanban-prefix()
 94: "Kanban prefix execution"
 95: ["Kanban"
 96:  ["Operations"
 97:   (kanban-init-exec)
 98:   (kanban-update-boards)]
 99:  [(kanban-row-up)
100:   (kanban-row-down)
101:   ]
102:  ["field" :class transient-column
103:   (kanban-field-left)]
104:  ["table" (kanban-field-up) (kanban-field-down)]
105:  ["edit" (kanban-field-right) ]
106:  ]
107: ["Options"
108:  (kanban-initialize-options)
109:  ]
110: )
111: 

Final menu output

prj_final.png

I created a Git repository for kanban-transient which you can find here: kanban-transient. If I have time, I’ll add more features, but the foundation is as shown in this post. Enjoy! 🪓

Resources

Date: 2025-04-23 Wed 00:00

Author: Calle Olsen

Created: 2025-08-04 Mon 21:30

Validate