Emacs Transient menu
Table of Contents
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).
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:
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 thetransient-scope
we get the value from the scope. - Line 11 - Using
setq
will make the variablecol/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
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")))
]
)
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")))
]]
)
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")))
]
]
)
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.
If for some reason you end up with really long names you might need to
use :pad-keys t
, this aligns the menus.
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.
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.
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.
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: ]])
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.
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
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.
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:
- Prompt :: The string shown in the minibuffer
- Initial-input :: what (if anything) to pre-fill as initial value in the prompt.
- 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
(seeC-h f read-file-name
) which returns a string , so no need for transformation.
The final output:
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:
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,
- goto start of the buffer
- Search forward forward for the dynamic block kanban
- if not found return error message (no kanban board found)
- when its found execute the
org-dblock-update
- 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
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! 🪓