smithery.ai

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.

First seen Mar 24, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 27,305 B
  • docs SUMMARY.md 224 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Emacs Transient Expert

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.

When to Use This Skill

Invoke this skill when:

  • Implementing new transient-define-prefix menus
  • Creating transient-define-suffix or transient-define-infix commands
  • Debugging transient menu layouts or behavior
  • Understanding transient levels, groups, or conditional display
  • Working on keyboard-driven UI similar to Magit
  • Converting traditional Emacs commands to transient interfaces

Core Concepts

What is Transient?

Transient is a library for creating keyboard-driven, temporary menus in Emacs. It's the foundation of Magit's interface and provides:

  • Display of current state - Show active arguments and options
  • Modal bindings - Temporary keymaps that disappear when done
  • Contextual UI - Menus adapt based on state
  • Persistence - Save and restore argument values across sessions
  • History - Track previously used values

Key Terminology

  • Prefix - The main transient command that opens the menu
  • Suffix - Commands invoked from the transient (actions)
  • Infix - Special suffixes that set arguments/options without

exiting

  • Groups - Organizational units for layout (rows, columns,

sections)

  • Levels (1-7) - Control visibility based on user expertise

(default: 4)

  • Scope - Contextual value passed to suffixes (e.g., current

branch)

Three Core Macros

1. transient-define-prefix

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 state

2. transient-define-suffix

Defines 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 disable

3. transient-define-infix (or transient-define-argument)

Defines 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")

Infix/Suffix Classes

Suffix Classes

  • transient-suffix - Base class for all suffixes
  • transient-infix - Base for all infixes (auto-stays transient)

Infix Classes (derive from transient-infix)

For command-line arguments:

  • transient-switch - Boolean flag (e.g., --verbose)
  • transient-option - Argument with value (e.g., --author=NAME)
  • transient-switches - Mutually exclusive options
  • transient-files - File arguments (-- separator)

For variables:

  • transient-variable - Base for variable infixes
  • transient-lisp-variable - Set Emacs Lisp variables

Display only:

  • transient-information - Display info (no command/key)
  • **transient-information*** - Info aligned with descriptions

Suffix Specification Syntax

Three ways to specify suffixes:

1. Inline (shorthand)

("key" "description" command)
("-s" "switch" "--switch")              ; Auto-creates transient-switch
("-o" "option" "--option=")             ; Auto-creates transient-option

2. With keyword arguments

("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

3. Reference to separately defined command

(my-custom-suffix)                      ; Uses suffix's own key/desc

Group Specification Syntax

[{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 horizontally
  • transient-columns - Contains column groups side-by-side
  • transient-subgroups - Contains subgroups

The :transient Slot (Controlling State)

Controls 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 prefix

For infixes (default: stay transient):

  • Infixes always use transient--do-stay by default
  • Rarely need to override

For sub-prefixes (nested transients):

  • nil - Exit all transients when sub-prefix exits
  • t - 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 args
  • transient--do-stay - Stay, don't export args
  • transient--do-call - Stay and export args
  • transient--do-return - Exit to parent prefix

Layout Patterns

Stacked Groups (Vertical)

(transient-define-prefix my-menu ()
  ["Top Group" ...]
  ["Bottom Group" ...])

Columns (Side-by-side)

(transient-define-prefix my-menu ()
  [["Left Column" ...]
   ["Right Column" ...]])

Mixed (Stacked + Columns)

(transient-define-prefix my-menu ()
  ["Top Group (full width)" ...]

  [["Left Column" ...]
   ["Right Column" ...]])

Grid Layout

(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)]])

Spacing

["Group"
 ""                                     ; Empty line
 ("k" "first" cmd)
 ("l" "second" cmd)
 ""                                     ; Another gap
 ("m" "third" cmd)]

Accessing Transient Values

In suffix commands

(transient-define-suffix my-suffix (args)
  "Do something with ARGS."
  (interactive (list (transient-args 'my-prefix)))
  ;; Now use args...
  (message "Args: %S" args))

Getting specific argument values

(let* ((args (transient-args 'my-prefix))
       (author (transient-arg-value "--author=" args))
       (verbose-p (transient-arg-value "--verbose" args)))
  ;; Use values...
  )

Using scope (contextual value)

(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)))

Conditional Display (Predicates)

Visibility predicates (if suffix should appear)

("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

Inapt predicates (show but grayed out)

("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

On groups

[:if magit-rebase-in-progress-p         ; Whole group conditional
 ("a" "abort" magit-rebase-abort)
 ("c" "continue" magit-rebase-continue)]

Important Suffix Slots

Required/Common:

  • :key - Key binding
  • :description - String or function returning string
  • :command - The command to invoke

Behavioral:

  • :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/disable

Display:

  • :format - Custom display format (%k %d %v)
  • :face - Face for description
  • :summary - Echo area/tooltip text

Help:

  • :show-help - Custom help function

Important Infix Slots

All 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 string

Multi-value:

  • :multi-value - 'rest or 'repeat for multiple values

Other:

  • :init-value - Function to set initial value
  • :history-key - Symbol for history (share across infixes)
  • :unsavable - Don't save with prefix value

Levels (1-7)

Control visibility based on user preference:

  • 1-3: Essential commands (always visible)
  • 4: Default level
  • 5-6: Advanced/rarely used
  • 7: Experimental/debug
  • 0: Never show (effectively disabled)
("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.

Dynamic Content

Dynamic descriptions

("k" my-cmd
 :description (lambda ()
                (format "Branch: %s" (magit-get-current-branch))))

Dynamic groups (:setup-children)

[: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 "."))))]

Information display

["Info"
 (:info "Static information")
 (:info (lambda () (format "Dynamic: %s" (current-time-string))))
 (:info my-info-function :format " %d")]  ; Custom format

Common Patterns from Magit

Context-aware suffixes

(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))))
  ...)

Shared argument groups

(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" ...])

Validation before execution

(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))

Best Practices

1. Use appropriate classes

  • Use transient-switch for boolean flags
  • Use transient-option for value-taking arguments
  • Use transient-switches for mutually exclusive options

2. Leverage levels effectively

  • Put common operations at level 3-4
  • Put advanced features at level 5-6
  • Use level 7 for debug/experimental

3. Provide good descriptions

  • Keep descriptions concise (fits in menu)
  • Use dynamic descriptions to show state
  • Use :summary for longer explanations

4. Handle state properly

  • Use :transient t for commands that should stay in menu
  • Use :if predicates instead of manual state checking
  • Use :inapt-if to show unavailable options

5. Organize logically

  • Group related items together
  • Use columns for parallel choices
  • Put common actions on left, advanced on right

6. Share history

  • Use :history-key to share history between similar infixes
(transient-define-argument my-author ()
  :argument "--author="
  :history-key 'my-package-author-history)

7. Provide help

  • Set :man-page or :info-manual on prefix
  • Use descriptive docstrings
  • Implement custom :show-help if needed

8. Test interactively

  • Use C-h while transient is active to see help
  • Use C-x l to experiment with levels
  • Use C-x s / C-x C-s to test persistence

Common Gotchas

  1. Don't quote in transient definitions - The macro handles it

```elisp ;; WRONG: ["Group" '("k" "desc" 'cmd)]

;; RIGHT: ["Group" ("k" "desc" cmd)] ```

  1. Use :transient t for iterative commands

``elisp ("n" "next" my-next :transient t) ; Can press 'n' repeatedly ``

  1. Remember infixes stay transient by default

- Don't need :transient t on infixes - They automatically use transient--do-stay

  1. :if vs :inapt-if

- :if - completely hide the suffix - :inapt-if - show but grayed out

  1. Accessing args in interactive

``elisp (interactive (list (transient-args 'my-prefix))) ; Correct ``

  1. Sub-prefix returns

``elisp ("s" "sub-menu" my-sub-prefix :transient t) ; Returns to parent ``

Complete Example

;; 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)]])

Reference Materials

For deeper understanding, refer to:

  • transient-reference/transient/ - Core library documentation
  • transient-reference/magit/ - Real-world usage in Magit
  • transient-reference/forge/ - Additional patterns in Forge
  • transient-reference/transient-showcase/ - Examples showcase

Testing Transient Menus

Why Test Transients?

Transient menus have complex state management involving:

  • Infix commands that set state variables
  • Suffix commands that read those states
  • Pre/post-command hooks for state transitions
  • Display refreshes and keybinding resolution

Testing only suffix commands (by mocking transient-args) skips testing the entire UI layer.

Two Testing Approaches

1. Unit Testing (Mocked Args) - Fast but Limited

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:

  • Fast (~0.5s per test)
  • Simple to write
  • Good for testing suffix logic

Cons:

  • ❌ Doesn't test infix commands
  • ❌ Doesn't test transient UI
  • ❌ Doesn't test user workflow
  • ❌ Won't catch binding errors

2. Integration Testing (execute-kbd-macro) - Slower but Complete

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:

  • ✅ Tests infix commands
  • ✅ Tests transient UI
  • ✅ Tests real user workflow
  • ✅ No mocking needed
  • ✅ Works in batch mode (CI-friendly!)
  • ✅ Catches binding errors, UI bugs

Cons:

  • Slower (~2.3s per test, +0.65s overhead)
  • More complex to write
  • Need to know key sequences

Performance breakdown:

  • Transient infrastructure: ~0.15s
  • Multiple kbd macro calls: ~0.20s
  • Minibuffer interactions: ~0.20s
  • Display refreshes: ~0.10s
  • Total overhead: ~0.65s per test

Critical Rule for execute-kbd-macro

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.

Testing Patterns

Pattern 1: Minimal workflow (title only)

(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...
    ))

Pattern 2: Full workflow (multiple fields)

(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
    ))

Pattern 3: Testing switches (toggle flags)

(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...
    ))

Pattern 4: Testing transient navigation

(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...
    ))

Helper Functions

(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

When to Use Each Approach

Use mocked approach when:

  • Testing suffix logic in isolation
  • Testing error handling
  • Speed is critical (large test suite)
  • Don't need to test UI interaction

Use execute-kbd-macro approach when:

  • Testing end-to-end user workflows
  • Validating infix commands work
  • Testing transient state management
  • Need confidence in the full UI
  • Testing for a critical user path

Recommended strategy:

  • Use mocked tests for most unit tests
  • Use execute-kbd-macro for key integration tests
  • At least one full UI test per transient menu

Example: Complete Test Suite

;;; 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...
    ))

Debugging Tips

When tests fail:

  1. Check key bindings - Use C-h in transient to see actual keys
  2. Test interactively first - Run the transient manually
  3. Add debug messages:

``elisp (execute-kbd-macro (kbd "t Test RET")) (message "After title: %S" (transient-args 'my-prefix)) ``

  1. Check transient buffer - Look at transient buffer state
  2. Use edebug - Step through suffix execution

Common issues:

  • "Unbound suffix" - Wrong key binding in test
  • "Wrong type argument" - Reader function got nil/wrong type
  • Macro fails silently - Split key + input across macros
  • Args not set - Forgot to call funcall-interactively on prefix

Testing Multiline Editor Fields

Fields 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:

  1. - d opens the editor buffer
  2. Full SPC description SPC text types into that buffer (it's now active)
  3. C-c C-c commits and returns to transient menu
  4. All happens in single macro execution, so context is preserved

What 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:

  • ✅ Simple minibuffer prompts work (combine key + input + RET)
  • ✅ Switches/toggles work (just the key)
  • ✅ Multiline editor fields work (combine key + text + C-c C-c)
  • ❌ Splitting any interaction across multiple macro calls fails

Comparison to Other Testing Approaches

with-simulated-input package:

  • ❌ Adds complexity without clear benefits for transient testing
  • ❌ Our pattern (mock transient-args + call-interactively) is simpler
  • ✅ execute-kbd-macro works natively in batch mode

Casual project approach:

  • Tests only binding structure (overrides commands with stubs)
  • ❌ Doesn't test actual command execution
  • ✅ Our approach tests REAL commands with REAL execution

Result: Our execute-kbd-macro approach gives superior coverage!

Version History

  • v1.1.0 (2025-11-19): Added comprehensive testing section with

execute-kbd-macro patterns and performance analysis

  • v1.0.0 (2025-11-06): Initial skill created from research of

transient, Magit, Forge, and transient-showcase repositories