Claude Code Plugins

Community-maintained marketplace

Feedback

emacs-transient

@r0man/beads.el
5
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name emacs-transient
description 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.

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

    ;; WRONG:
    ["Group" '("k" "desc" 'cmd)]
    
    ;; RIGHT:
    ["Group" ("k" "desc" cmd)]
    
  2. Use :transient t for iterative commands

    ("n" "next" my-next :transient t)    ; Can press 'n' repeatedly
    
  3. Remember infixes stay transient by default

    • Don't need :transient t on infixes
    • They automatically use transient--do-stay
  4. :if vs :inapt-if

    • :if - completely hide the suffix
    • :inapt-if - show but grayed out
  5. Accessing args in interactive

    (interactive (list (transient-args 'my-prefix)))  ; Correct
    
  6. Sub-prefix returns

    ("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 john@example.com 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:
    (execute-kbd-macro (kbd "t Test RET"))
    (message "After title: %S" (transient-args 'my-prefix))
    
  4. Check transient buffer - Look at *transient* buffer state
  5. 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