Automating Work with Transient: An Intermediate Example
Note
If you already know what transient does, and just want to see my example of automating real work with it, skip this section.
Even folks who have never used Emacs before have often heard of magit, one of the finest examples of a text-based interface for a complex tool ever created. Even if you disagree, you don't have to search for long to find copious examples of comments online along the lines of "magit is the primary reason I even use Emacs".
Folks who have used magit in anger have likely encountered
transient in some capacity,
which is the library on which magit builds its menus, factored out to
allow other projects to use the same functionality. It's a
fantastically useful library, and something that deserves more
attention. In the same way that magit wraps git
, you can use
transient to wrap your frequently-used CLI tools with surprisingly
little work, and make them more configurable and more accessible
within Emacs.
It's true that transient is already relatively widely used, but so far primarily by Emacs insiders and established package developers. But there are lots of Emacs users—and I consider myself among them—who are Emacs end-users rather than Emacs Lisp programmers and contributors. My claim is that even those users can build something genuinely useful without necessarily needing to immerse themselves in Emacs internals.
My journey with transient started with the fabulous interactive tutorial, good enough to be recommended even in the official documentation. I have no ambition to supersede that tutorial, but my experience was that it seems to move from simple to complex examples in a single step without any meaningful transition. I just want to show a single intermediate use case, as a kind of missing example. If you don't know what transient prefixes, infixes, and suffixes are, start with that tutorial then come back here when hit a wall.
The ideal targets for transient-based wrappers are CLI tools that you run frequently, have useful configuration options in the form of command-line arguments and switches, and aren't quite popular enough that they already have a fabulous Emacs package dedicated to them.
My intermediate example focuses on tooling used to build and maintain
.deb
packages for Ubuntu and Debian, but in retrospect it's easy to
think of examples that would be equally good candidates:
- You use tools like
ffmpeg
to get your own video content ready to publish, and don't want to remember all the arcane flags to do things like extracting audio from a video file. - You're running a system with
systemd
, and frequently want to run a number ofsystemctl
commands with custom patterns to interact with system services. - Maybe you're a Rust programmer and want to run different kinds of
builds with things like unstable flags (
-Z
) or without modifying a lockfile (--locked
).
First Steps with dpkg-buildpackage
Part of the dpkg
(Debian package) family,
dpkg-buildpackage
is a tool for building deb
packages from source.
It's man
page is more than 500 lines, almost entirely focused on
configuration options for specific packaging use-cases. I frequently
invoke something like pkg-buildpackage -S -I -i -nc -d -sa
. That's
not a contrived example, it's extremely common to have 5+ opaque flags
and arguments like this.
Let's start building a transient "prefix", which more-or-less corresponds to the menu popup itself, that lets us set the build type. There are basically three kinds of builds:
- source builds (the
-S
flag) - architecture-specific binary builds (the
-B
flag) - architecture-independent binary builds (the
-A
flag)
There are also flags for basically every combination of these, but with transient we don't need to care about those because we can build combinations in the UI. This prefix intentionally makes these non-exclusive.
If you evaluate that and run it directly with M-x dpkg-buildpackage-transient
, you should see something like this:

And, unsurprisingly, you can type -S
or -A
to change which options
are enabled.
A Custom "infix", a.k.a. the Interesting Part
Next I wanted to add the -i
and -I
flag, which allow configuration
related to ignoring certain files, but they are a bit unorthodox.
Typical CLI flags come in two core forms, namely those that are
standalone flags or "switches" (something like --verbose
), and those
which indicate some data is to follow (something like
--author=<name>
). These -i
and -I
flags are a hybrid of the
two, and support three different states. They can be not passed at
all, they can be passed as switches with no data, or they can be
immediately followed by an ignore pattern. So you might see -I -i<some_pattern>
in a call.
Not being able to find a built-in class to represent this in transient, I determined that I needed to build a custom infix. The tutorial linked above actually has a thorough example for custom infixes, but it's just code with no explanation, and needlessly complicated because it deals with hydration and serialization while introducing the core concepts. That complexity is the main reason this post exists.
This section, out of necessity. Don't be intimidated. You don't need to be an Emacs Lisp expert to understand this example, I will explain all the code as thoroughly as I can, and if you're a programmer in any other language I hope to enable you to follow along.
Most of the complexity comes from the need to use the Emacs EIEIO system, which is a kind of object-oriented framework based on one developed for Common Lisp. Documentation for that is also scattered, but if you've used an OO language before (which I'm assuming about you, dear reader) then the basics are simple.
The first thing we need to do is define a new class. It will inherit
most of what it needs from transient-infix
, which is a class defined
by transient.
We now need to override some of the parent class' methods to define our functionality. At a high level, we need functionality to:
- transition between states
- format the stored value for parsing and usage in code
- format the stored value for human understanding
- (optionally) set a specific starting state
Transition Between States
If you look at the transient documentation, you'll see that infix
commands, generally, are defined as follows. Don't worry about
knowing these functions. The things I want you to notice are the
references to transient-infix-set
and transient-infix-read
, which
are EIEIO methods (actually just plain Emacs Lisp functions, but following a
specific pattern to allow EIEIO usage).
Ignoring what's around those two methods, you can see a let
expression that roughly says "get the command object itself, call
transient-infix-read
on it, and whatever value that provides, call
transient-infix-set
to set it as the new value.
Those two functions are "generic", which means they are intended to be overridden with the EIEIO system. We don't actually need to deal with the setter function, because the default is good enough for us. The read function is the interesting one, it's where we need to get a new value for the object based on the old one, corresponding to our 3-state option.
Here's my code.
We're defining a new overridden method for transient-tristate
, our
class. The code works like this. First, we use a let
expression to
grab the existing value. The oref
function grabs a field from an
object in EIEIO, and so here were asking for the value
field (they
call it a slot, in case you see that word somewhere). It comes from
the parent class, we never needed to define it.
Then we use pcase
, which does pattern matching on the value we just
extracted. Since there are 3 states, there are three patterns. The
first is just 'nil
, meaning value
is empty. This corresponds to
the "not passed as a flag at all" case. If we match that, we return
the value t
, which in Emacs Lisp is the value that more or less
corresponds to "true" or "not nil". That's how we define our "turned
on, but with no explicit pattern".
Our second case is designed to match exactly scenario. We match
against the symbol 't
, and if we find it, we want to enter the state
where we are "on, and with a pattern". To achieve that I've used the
read-string
function, which is a simple way to prompt the user for a
string in a minibuffer in Emacs. The argument being passed is a
prompt, which may appear complex. You could just write "Pattern: ",
but since I wanted this to be reusable, I didn't want to hardcode
that. Instead, I'm once again using the EIEIO system and getting the
"description"
field of our object. This is something that all infix
objects have, and it corresponds to the text description you see in
the actual menu. So this code just means, prompt the user for a
string by printing out the description of the object followed by ": "
. The string the user provides is returned by the function.
Finally, in our third case, we theoretically want to match on an
arbitrary string value. We could be fancy, but since it's our only
remaining case, we can use the wildcard pattern _
. It will match
anything that didn't match case 1 or 2. In that scenario we just want
to cycle back to empty, so we return nil.
And that alone, is the bulk of functionality for this 3-state option,
but I continued from here because I wanted this code to be reusable
and generic. It will currently just return t
or a string with no
indication of which flag it's associated with. That makes it hard to
ultimately turn this into the format we want, with -I
or -i
.
Format the Stored Value for Parsing and Usage in Code
In order to control how a value is represented when a transient suffix
asks for it, we can override the transient-infix-value
method. My
code follows, which is basically identical in structure to the last one.
The let
binds two EIEIO fields from our object. The first is the
value, exactly as we did before. The second is the "argument" field,
which corresponds to one of the 3 values you see in most uses of
transient.
For instance, we previously wrote this:
For our 3 infix definitions, the "argument" corresponds to the third
value in the lists, namely "-S"
, "-B"
, and "-A"
. We haven't
defined our -I
and -i
versions yet, but we can decide now that
we'll set the argument to be exactly -I
and -i
in order to make
our lives easier, since we're dealing with the arguments in a custom
way.
Now we enter the cond
expression, which again matches our 3 states.
For the 'nil
case, we don't want to pass anything, so we just return
'nil
. For the 't
case we return the argument. That means we
return the value '"-I"' or '"-i"' depending on what's stored in our
object. And finally, if we have data, we use the format
method to
concatenate two strings together, the argument and the data being
stored. So that means if value
contains "pattern"
and argument
contains "-I"
, we return "-Ipattern"
, which is what
dpkg-buildpackage
expexts.
Format the Stored Value for Human Understanding
We can now, optionally, make our value look nice for users (or for
ourselves). The formatted value in the transient need not be the same
as what's passed to a suffix in code. To do that, we can override
transient-format-value
. Here's my code again, which might look
familiar at this point:
We again start with a let
expression to grab the value out of the
object. Skipping a line, we see another familiar 3-condition pcase
.
In this case we're returning string representations. I'm not a
designer, but I've tried to be thoughtful, and so used a unicode
checkmark representation to mean "on", with the pattern data appended
when it exists. So we get either an empty checkbox, a filled
checkbox, or a filled checkbox with extra data.
I am also using propertize
to set how the text is displayed. If the
value is nil
I applied the existing face transient-inactive-value
and if it's in either "on" state, I set transient-value
. This is
not necessary, but gives some nice, extra visual indication of the
current state.
Back to Our Transient
We are now ready to use our totally custom infix object to add -I
and -i
to our transient! All we really need to do is add these lines:
These look just like the infixes you have already seen, just with the
:class
keyword followed by our custom class name. Don't forget that
we're depending on those third values to be "-I"
and "-i"
for our
formatting.
If you update your transient, you should now see something like this,
depending on the headers and layout you choose (out of scope here).
Notice how I've toggled mine to be "on" and "on with the pattern *.*
(not a useful pattern!)".

Set a Specific Starting State
One very last step you can take, should you so choose, is to set a
default value for your custom infixes. For instance, most of the time
I run dpkg-buildpackage
, I want to pass -I
and -i
with no data.
For built-in arguments and switches this is easy. You can just add a
:value
definition at the top of your transient:
By setting the value to be '("-S")'
, transient knows that the -S
flag should default to on.
However, this gets harder with our custom infix definition. Remember,
our internal value is just the value 'nil
, 't
, or the extra data.
Transient won't know what to do if we have some initial state with
't
in it because it doesn't match the "argument" value of any of our
definitions.
We could refactor everything to store the argument and data both, but
this would complicate our pcase
patterns, impose extra requirements
on the user to define arguments in a particular, systematic way, and
require me to rewrite a big chunk of this post. Instead, we're just
going to override some more functionality to help transient compute an
initial value.
The method we want is called transient-init-value
. Here's my
current definition, which probably reveals more than I'd prefer about
my poor Emacs Lisp skills.
We grab the value of argument
again, and we also grab the value
of
the prefix object. This is not the same thing that we've been doing,
despite the overloaded names. The value of the prefix object is that
thing we set with the :value
keyword pair, namely something like
("-S" "-I" "-i")
. So we have some work to do to turn that into a
value for our actual object.
I've used seq-find
, which is a built-in way to scan through a list
of things until an element matches the supplied predicate function.
Our predicate function is an anonymous function which checks that it's
argument is a string and, if so, checks whether the value of
argument
is a prefix of that string. In a concrete run of this
code, that might mean it's looking to see if "-I"
is a prefix of
"-Ipattern"
, which would return true and so cause seq-find
to
finish execution and give us that element.
Based on what that find operation returns, we then set the initial
value of our oibject to one of 3 things, using a cond
, which is just
like a pcase
but uses predicate functions that return true to
indicate a case match instead of pattern matching the structure.
The first case says that argument
is itself a member of the initial
state value. That would be the case if it finds exactly "-I"
, for
instance. The second case matches when found
is not nil
, meaning
we found the argument
as a prefix somewhere in our value
, but with
other data attached to it. In that case, we use substring to grab
just the extra data by dropping the characters from the argument
.
Finally, if we don't find out argument
at all, we just set nil
.
And with that, our transient is basically complete!
Finishing Up
All of our argument handling is in place, but this isn't actually
calling out to dpkg-buildpackage
yet, of course. What we need to do
is add a trivial suffix to our transient. The details are out of
scope, but something like:
Where prep-dpkg-buildpackage
puts all the arguments together into a
command and calls it with the compile
function. In my case, I
wanted some extra buffer naming customizations to let me run commands
in parallel, and also to prompt for a working directory. Those things
are out of scope, but you are welcome to poke through my config to see
the code:
https://github.com/karljs/dotfiles/blob/main/emacs.d/config/kjs-deb.el
Summary
Thanks for reading this. It's intended for a very specific audience, namely folks who understand a tiny bit of transient, but want to learn a little more without taking multiple steps in one go. If you are in that audience, or aspire to be, I hope this helps you use transient to build your own little Emacs-based automations to make your life and job easier!
As always, feel free to reach out if you see something I've done incorrectly or non-idiomatically, or just have questions.