xiaohongshu
xiaohongshu
emacs-transient
Expert guide for building Emacs transient menus (keyboard-driven UI like Magit). Use when implementing or debugging transient-define-prefix, transient-define-suffix, or transient-define-infix commands.
Full skill instructions
This skill provides comprehensive guidance for building transient-based interfaces in Emacs Lisp, based on the official transient library and real-world implementations in Magit, Forge, and transient-showcase.
Invoke this skill when:
transient-define-prefix menustransient-define-suffix or transient-define-infix commandsTransient is a library for creating keyboard-driven, temporary menus in Emacs. It's the foundation of Magit's interface and provides:
Defines the main transient menu.
(transient-define-prefix my-menu ()
"Description of what this menu does."
:man-page "git-commit" ; Optional: link to man page
:info-manual "(magit)Committing" ; Optional: link to info manual
:value '("--verbose") ; Optional: default arguments
;; Groups define layout
["Arguments" ; Group header
("-v" "Verbose" "--verbose") ; Switch (toggle)
("-a" "Author" "--author=" ; Option (takes value)
:prompt "Author: ")
(my-custom-infix)] ; Reference to defined infix
[["Actions" ; Nested groups = columns
("c" "Commit" my-commit-cmd)
("a" "Amend" my-amend-cmd)]
["Other"
("q" "Quit" transient-quit)]])
Key slots:
:value - Default arguments:man-page - Man page for help:info-manual - Info manual section:transient-suffix - Default transient behavior for suffixes:transient-non-suffix - Allow/forbid non-suffix commands:refresh-suffixes - When to refresh suffix stateDefines action commands (suffixes).
(transient-define-suffix my-commit-cmd (args)
"Create a commit with ARGS."
:description "Commit staged changes" ; Optional: override in menu
:transient t ; Stay transient after calling
(interactive (list (transient-args 'my-menu)))
(apply #'my-run-git "commit" args))
Key slots:
:key - Key binding (can override menu binding):description - Can be string or function returning string:transient - Control transient state (see below):if / :if-not - Conditional visibility:inapt-if / :inapt-if-not - Show but disableDefines argument commands (infixes).
(transient-define-argument my-author-arg ()
:description "Set author"
:class 'transient-option ; Option class (takes value)
:shortarg "-a" ; Short form
:argument "--author=" ; Long form
:reader #'my-read-author) ; Custom reader function
(transient-define-infix my-verbose-switch ()
:description "Verbose output"
:class 'transient-switch ; Switch class (boolean)
:argument "--verbose")
transient-suffix - Base class for all suffixestransient-infix - Base for all infixes (auto-stays transient)For command-line arguments:
transient-switch - Boolean flag (e.g., --verbose)transient-option - Argument with value (e.g., --author=NAME)transient-switches - Mutually exclusive optionstransient-files - File arguments (-- separator)For variables:
transient-variable - Base for variable infixestransient-lisp-variable - Set Emacs Lisp variablesDisplay only:
transient-information - Display info (no command/key)transient-information* - Info aligned with descriptionsThree ways to specify suffixes:
("key" "description" command)
("-s" "switch" "--switch") ; Auto-creates transient-switch
("-o" "option" "--option=") ; Auto-creates transient-option
("key" "description" command
:transient t ; Stay transient
:if (lambda () (my-condition))) ; Conditional
("-a" "author" "--author="
:prompt "Author name: "
:reader #'my-custom-reader
:always-read t) ; Always prompt, don't toggle
(my-custom-suffix) ; Uses suffix's own key/desc
[{LEVEL} {DESCRIPTION} {KEYWORD VALUE}... ELEMENT...]
Common patterns:
["Group Title" ; Simple group with title
("k" "desc" cmd)]
[:description "Dynamic" ; Dynamic description
:description (lambda () (format "Time: %s" (current-time-string)))
("k" "desc" cmd)]
[:if some-predicate ; Conditional group
("k" "desc" cmd)]
[:class transient-row ; Explicit layout class
("k" "desc" cmd)]
[["Column 1" ; Nested groups = columns
("a" "cmd a" cmd-a)]
["Column 2"
("b" "cmd b" cmd-b)]]
Group classes:
transient-column - Stack items vertically (default)transient-row - Arrange items horizontallytransient-columns - Contains column groups side-by-sidetransient-subgroups - Contains subgroupsControls whether transient stays active after invoking a command.
For suffixes (default: exit transient):
nil or :transient nil - Exit transient (default for suffixes)t or :transient t - Stay transient:transient 'transient--do-call - Export args and stay:transient 'transient--do-return - Return to parent prefixFor infixes (default: stay transient):
transient--do-stay by defaultFor sub-prefixes (nested transients):
nil - Exit all transients when sub-prefix exitst - Return to parent when sub-prefix exits:transient 'transient--do-recurse - Enable return behavior:transient 'transient--do-replace - Replace parent (no return)Common pre-commands:
transient--do-exit - Exit and export argstransient--do-stay - Stay, don't export argstransient--do-call - Stay and export argstransient--do-return - Exit to parent prefix(transient-define-prefix my-menu ()
["Top Group" ...]
["Bottom Group" ...])
(transient-define-prefix my-menu ()
[["Left Column" ...]
["Right Column" ...]])
(transient-define-prefix my-menu ()
["Top Group (full width)" ...]
[["Left Column" ...]
["Right Column" ...]])
(transient-define-prefix my-menu ()
[:description "The Grid"
["Left Column"
("tl" "top-left" cmd)
("bl" "bottom-left" cmd)]
["Right Column"
("tr" "top-right" cmd)
("br" "bottom-right" cmd)]])
["Group"
"" ; Empty line
("k" "first" cmd)
("l" "second" cmd)
"" ; Another gap
("m" "third" cmd)]
(transient-define-suffix my-suffix (args)
"Do something with ARGS."
(interactive (list (transient-args 'my-prefix)))
;; Now use args...
(message "Args: %S" args))
(let* ((args (transient-args 'my-prefix))
(author (transient-arg-value "--author=" args))
(verbose-p (transient-arg-value "--verbose" args)))
;; Use values...
)
(transient-define-prefix my-menu (scope)
"Menu with scope."
["Actions"
("a" "Action" my-action)]
(interactive "P") ; Can take prefix arg as scope
(transient-setup 'my-menu nil nil :scope scope))
(transient-define-suffix my-action ()
(interactive)
(let ((scope (transient-scope)))
(message "Scope: %S" scope)))
("k" "desc" cmd
:if (lambda () (file-exists-p "Makefile"))) ; Show if true
("k" "desc" cmd
:if-not some-mode ; Show if not in mode
:if-non-nil some-variable ; Show if var non-nil
:if-mode 'emacs-lisp-mode ; Show in mode
:if-derived 'prog-mode) ; Show if derived
("k" "desc" cmd
:inapt-if (lambda () (not (magit-anything-staged-p)))) ; Gray if true
("k" "desc" cmd
:inapt-if-not some-function ; Gray if false
:inapt-if-nil some-variable) ; Gray if var nil
[:if magit-rebase-in-progress-p ; Whole group conditional
("a" "abort" magit-rebase-abort)
("c" "continue" magit-rebase-continue)]
Required/Common:
:key - Key binding:description - String or function returning string:command - The command to invokeBehavioral:
:transient - Stay transient? (t/nil/pre-command):level - Visibility level (1-7, default 4)Conditional:
:if, :if-not, :if-mode, :if-derived - Visibility:inapt-if, :inapt-if-not - Enable/disableDisplay:
:format - Custom display format (%k %d %v):face - Face for description:summary - Echo area/tooltip textHelp:
:show-help - Custom help functionAll suffix slots, plus:
Argument-related:
:argument - Long form (e.g., --verbose):shortarg - Short form (e.g., -v):class - Infix class (switch/option/etc.)Reading values:
:reader - Function to read value (PROMPT, INITIAL, HISTORY):prompt - Prompt string or function:choices - List of valid values:always-read - Always prompt (don't toggle for options):allow-empty - Allow empty stringMulti-value:
:multi-value - 'rest or 'repeat for multiple valuesOther:
:init-value - Function to set initial value:history-key - Symbol for history (share across infixes):unsavable - Don't save with prefix valueControl visibility based on user preference:
("k" "advanced" cmd :level 6) ; Only show at level 6+
["Arguments" :level 5 ; Whole group at level 5
("-v" "verbose" "--verbose")]
Users can change levels interactively with C-x l.
("k" my-cmd
:description (lambda ()
(format "Branch: %s" (magit-get-current-branch))))
[:class transient-column
:setup-children
(lambda (_)
(transient-parse-suffixes
'my-prefix
(mapcar (lambda (file)
(list (substring file 0 1) file
(lambda () (interactive) (find-file file))))
(directory-files "."))))]
["Info"
(:info "Static information")
(:info (lambda () (format "Dynamic: %s" (current-time-string))))
(:info my-info-function :format " %d")] ; Custom format
(transient-define-suffix my-cmd (args)
(interactive
(if (derived-mode-p 'my-list-mode)
(list (my-get-args-from-buffer))
(list (transient-args 'my-prefix))))
...)
(transient-define-group my-common-args ()
["Common Arguments"
("-v" "Verbose" "--verbose")
("-q" "Quiet" "--quiet")])
(transient-define-prefix my-menu-1 ()
[my-common-args] ; Include by reference
["Actions" ...])
(transient-define-prefix my-menu-2 ()
[my-common-args] ; Reuse in another menu
["Other Actions" ...])
(transient-define-suffix my-create (title desc)
(interactive
(list (read-string "Title: ")
(read-string "Description: ")))
(when (string-empty-p title)
(user-error "Title cannot be empty"))
(my-create-thing title desc))
transient-switch for boolean flagstransient-option for value-taking argumentstransient-switches for mutually exclusive options:summary for longer explanations:transient t for commands that should stay in menu:if predicates instead of manual state checking:inapt-if to show unavailable options:history-key to share history between similar infixes(transient-define-argument my-author ()
:argument "--author="
:history-key 'my-package-author-history)
:man-page or :info-manual on prefix:show-help if neededC-h while transient is active to see helpC-x l to experiment with levelsC-x s / C-x C-s to test persistenceDon't quote in transient definitions - The macro handles it
;; WRONG:
["Group" '("k" "desc" 'cmd)]
;; RIGHT:
["Group" ("k" "desc" cmd)]
Use :transient t for iterative commands
("n" "next" my-next :transient t) ; Can press 'n' repeatedly
Remember infixes stay transient by default
:transient t on infixestransient--do-stay:if vs :inapt-if
:if - completely hide the suffix:inapt-if - show but grayed outAccessing args in interactive
(interactive (list (transient-args 'my-prefix))) ; Correct
Sub-prefix returns
("s" "sub-menu" my-sub-prefix :transient t) ; Returns to parent
;; Custom argument
(transient-define-argument my-pkg:--author ()
:description "Override author"
:class 'transient-option
:shortarg "-a"
:argument "--author="
:reader #'my-read-author)
;; Suffix that stays transient
(transient-define-suffix my-pkg-preview ()
"Preview current settings."
:transient t
(interactive)
(message "Args: %S" (transient-args 'my-pkg-create)))
;; Main suffix
(transient-define-suffix my-pkg-execute (args)
"Execute with ARGS."
(interactive (list (transient-args 'my-pkg-create)))
(apply #'my-pkg-run args))
;; Main menu
(transient-define-prefix my-pkg-create ()
"Create something with options."
:man-page "my-tool"
:value '("--verbose")
["Arguments"
("-v" "Verbose" "--verbose")
("-q" "Quiet" "--quiet")
("-n" "Dry run" "--dry-run" :level 5)
(my-pkg:--author)]
[["Actions"
("p" "Preview" my-pkg-preview)
("c" "Create" my-pkg-execute)]
["Other"
("q" "Quit" transient-quit)]])
For deeper understanding, refer to:
Transient menus have complex state management involving:
Testing only suffix commands (by mocking transient-args) skips
testing the entire UI layer.
Test suffix commands directly by mocking transient-args:
(defmacro my-test-with-transient-args (prefix args &rest body)
"Execute BODY with transient-args mocked for PREFIX to return ARGS."
(declare (indent 2))
`(cl-letf (((symbol-function 'transient-args)
(lambda (p)
(when (eq p ,prefix)
,args))))
,@body))
(ert-deftest my-test-suffix-execution ()
"Test suffix command with mocked args."
(my-test-with-transient-args 'my-prefix
'("--title=Test" "--priority=1")
(let ((result (call-interactively #'my-suffix-execute)))
(should (string-match-p "Created" result)))))
Pros:
Cons:
Test the full user interaction with keyboard macros:
(ert-deftest my-test-full-ui-interaction ()
"Test complete user workflow through transient UI."
:tags '(:integration :ui)
(my-test-with-project () ; Setup test environment
;; Invoke the transient menu
(funcall-interactively #'my-prefix)
;; Set title via infix (key + input in ONE macro!)
(execute-kbd-macro (kbd "t Test SPC Title RET"))
;; Set priority via infix
(execute-kbd-macro (kbd "- p 1 RET"))
;; Execute the suffix
(execute-kbd-macro (kbd "x"))
;; Verify results
(let ((result (my-get-created-item)))
(should (equal (plist-get result :title) "Test Title"))
(should (equal (plist-get result :priority) 1)))))
Pros:
Cons:
Performance breakdown:
You MUST combine transient key + input in a SINGLE kbd macro:
;; ✅ CORRECT - Key + input in one macro
(execute-kbd-macro (kbd "t Bug SPC Title RET"))
;; ❌ WRONG - Splitting key and input
(execute-kbd-macro (kbd "t")) ; Opens minibuffer
(execute-kbd-macro (kbd "Bug Title RET")) ; Fails! Tries to invoke
; transient keys B, u, g
Why? When you split the macro, the second call doesn't go to the minibuffer - it's interpreted as more transient commands.
(ert-deftest my-test-minimal-workflow ()
"Test simplest possible workflow."
(my-test-setup ()
(funcall-interactively #'my-create)
(execute-kbd-macro (kbd "t Minimal SPC Test RET"))
(execute-kbd-macro (kbd "x"))
;; Verify...
))
(ert-deftest my-test-full-workflow ()
"Test complete workflow with all fields."
(my-test-setup ()
(funcall-interactively #'my-create)
;; Set all fields
(execute-kbd-macro (kbd "t Full SPC Test RET"))
(execute-kbd-macro (kbd "- t feature RET"))
(execute-kbd-macro (kbd "- p 2 RET"))
(execute-kbd-macro (kbd "- a [email protected] RET"))
;; Execute
(execute-kbd-macro (kbd "x"))
;; Verify all fields were set correctly
))
(ert-deftest my-test-switches ()
"Test boolean switch toggling."
(my-test-setup ()
(funcall-interactively #'my-prefix)
;; Toggle switch on
(execute-kbd-macro (kbd "- v")) ; --verbose
;; Toggle switch off
(execute-kbd-macro (kbd "- v"))
;; Toggle back on
(execute-kbd-macro (kbd "- v"))
(execute-kbd-macro (kbd "x"))
;; Verify switch state...
))
(ert-deftest my-test-navigation ()
"Test moving between transient levels."
(my-test-setup ()
(funcall-interactively #'my-prefix)
;; Change transient level to show advanced options
(execute-kbd-macro (kbd "C-x l 6 RET"))
;; Now advanced options should be visible
(execute-kbd-macro (kbd "- n")) ; Advanced option
(execute-kbd-macro (kbd "x"))
;; Verify...
))
(defun my-test-kbd-do (keys)
"Execute keyboard macro from KEYS list.
KEYS is a list of key sequence strings that will be joined
and executed as a keyboard macro."
(execute-kbd-macro (kbd (string-join keys " "))))
;; Usage:
(my-test-kbd-do '("t" "Title" "RET")) ; Cleaner than raw kbd
Use mocked approach when:
Use execute-kbd-macro approach when:
Recommended strategy:
;;; Unit Tests (Mocked - Fast)
(ert-deftest my-create-test-parse-args ()
"Test argument parsing."
;; Fast unit test, no UI
)
(ert-deftest my-create-test-validation ()
"Test validation logic."
(my-test-with-transient-args 'my-create
'("--title=") ; Empty title
(should-error (call-interactively #'my-create-execute)
:type 'user-error)))
;;; Integration Tests (Full UI - Comprehensive)
(ert-deftest my-create-test-full-ui-basic ()
"Test basic creation workflow through UI."
:tags '(:integration :ui)
(my-test-setup ()
(funcall-interactively #'my-create)
(execute-kbd-macro (kbd "t Basic SPC Test RET"))
(execute-kbd-macro (kbd "x"))
;; Verify creation...
))
(ert-deftest my-create-test-full-ui-all-fields ()
"Test creation with all fields through UI."
:tags '(:integration :ui :slow)
(my-test-setup ()
(funcall-interactively #'my-create)
(execute-kbd-macro (kbd "t Full SPC Test RET"))
(execute-kbd-macro (kbd "- t feature RET"))
(execute-kbd-macro (kbd "- p 2 RET"))
(execute-kbd-macro (kbd "x"))
;; Verify all fields...
))
When tests fail:
C-h in transient to see actual keys(execute-kbd-macro (kbd "t Test RET"))
(message "After title: %S" (transient-args 'my-prefix))
*transient* buffer stateCommon issues:
funcall-interactively on prefixFields that open dedicated editor buffers for multiline input (description,
notes, comments) DO work with execute-kbd-macro using the same principle
as simple fields: combine everything in a SINGLE macro.
Pattern:
(funcall-interactively #'my-create)
;; ✅ CORRECT - Combine infix + text + commit in ONE macro
(execute-kbd-macro (kbd "- d Full SPC description SPC text C-c C-c"))
(execute-kbd-macro (kbd "- A Acceptance SPC criteria C-c C-c"))
Why this works:
- d opens the editor bufferFull SPC description SPC text types into that buffer (it's now active)C-c C-c commits and returns to transient menuWhat DOESN'T work (split across macros):
;; ❌ WRONG - Split into separate macros
(execute-kbd-macro (kbd "- d")) ; Opens editor
(execute-kbd-macro (kbd "My description")) ; Fails! Wrong context
(execute-kbd-macro (kbd "C-c C-c")) ; Fails! Wrong context
When you split into multiple execute-kbd-macro calls, each call starts from
the current buffer context, which may not be the editor buffer that was opened.
Summary:
with-simulated-input package:
Casual project approach:
Result: Our execute-kbd-macro approach gives superior coverage!
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience