Subject: [PATCH 1/5] Switch to transient

From: Nicolas Graves <[email-masked]>
Date: Mon, 3 Mar 2025 03:09:16 +0100

---
 README                  |   2 +-
 configure.ac            |  16 +--
 doc/emacs-guix.texi     |  55 +++++-----
 doc/htmlxref.cnf        |   6 +-
 elisp/guix-command.el   | 114 ++++++++++----------
 elisp/guix-help.el      |   2 +-
 elisp/guix-popup.el     | 227 ----------------------------------------
 elisp/guix-transient.el | 187 +++++++++++++++++++++++++++++++++
 elisp/guix.el           |   2 +-
 elisp/local.mk          |   6 +-
 10 files changed, 288 insertions(+), 329 deletions(-)
 delete mode 100644 elisp/guix-popup.el
 create mode 100644 elisp/guix-transient.el

diff --git a/README b/README
index 55a6956..6a63f97 100644
--- a/README
+++ b/README
@@ -34,7 +34,7 @@ In short, Emacs-Guix provides the following features:
 
   + [[/gnu/store]] items
 
-- Magit-like popup interface for all Emacs-Guix and Guix shell commands
+- Magit-like keyboard-driven menu for all Emacs-Guix and Guix shell commands
   (=M-x guix=).
 
 - Modes to view logs of package builds (=guix-build-log-mode= and
diff --git a/configure.ac b/configure.ac
index dd1de4b..3461bb3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -130,18 +130,18 @@ AC_ARG_WITH([editindirect-lispdir],
   [editindirectlispdir="no"])
 AC_SUBST([editindirectlispdir])
 
-AC_ARG_WITH([popup-lispdir],
-  [AS_HELP_STRING([--with-popup-lispdir],
-    [directory with magit-popup.el file])],
-  [popuplispdir="$withval"],
-  [popuplispdir="no"])
-AC_SUBST([popuplispdir])
+AC_ARG_WITH([transient-lispdir],
+[AS_HELP_STRING([--with-transient-lispdir],
+  [directory with transient.el file])],
+  [transientlispdir="$withval"],
+  [transientlispdir="no"])
+AC_SUBST([transientlispdir])
 
 AM_CONDITIONAL([GEISER_DIR], [test "x$geiserlispdir" != "xno"])
 AM_CONDITIONAL([DASH_DIR], [test "x$dashlispdir" != "xno"])
 AM_CONDITIONAL([BUI_DIR], [test "x$builispdir" != "xno"])
 AM_CONDITIONAL([EDITINDIRECT_DIR], [test "x$editindirectlispdir" != "xno"])
-AM_CONDITIONAL([POPUP_DIR], [test "x$popuplispdir" != "xno"])
+AM_CONDITIONAL([TRANSIENT_DIR], [test "x$transientlispdir" != "xno"])
 
 dnl If all elisp dependencies are specified, we can use "emacs -Q" for
 dnl byte-compilation.  Otherwise, "emacs" will be used, and it will
@@ -151,7 +151,7 @@ AM_CONDITIONAL([EMACS_Q],
         "x$dashlispdir" != "xno" -a \
         "x$builispdir" != "xno" -a \
         "x$editindirectlispdir" != "xno" -a \
-        "x$popuplispdir" != "xno"])
+        "x$transientlispdir" != "xno"])
 
 dnl ----------------------------------------------------------------
 
diff --git a/doc/emacs-guix.texi b/doc/emacs-guix.texi
index 3219354..6bb3810 100644
--- a/doc/emacs-guix.texi
+++ b/doc/emacs-guix.texi
@@ -53,7 +53,7 @@ A copy of the license is available at
 * System::              Interface for @code{operating-system} and services.
 * Store Items::         Interface for store items.
 * Package Licenses::    Interface for licenses of packages.
-* Popup Interface::     Magit-like interface for Emacs-Guix commands.
+* Keyboard-driven Menu::     Magit-like interface for Emacs-Guix commands.
 * Prettify Mode::       Abbreviating @file{/gnu/store/@dots{}} file names.
 * Prettify Variables::  Split and indent Shell variables.
 * Build Log Mode::      Highlighting Guix build logs.
@@ -77,8 +77,8 @@ Indexes
 Emacs-Guix (also known as ``guix.el'') provides various interfaces and
 tools related to the GNU Guix package manager.
 
-Call @kbd{M-x guix} if you prefer to dive in right away (@pxref{Popup
-Interface}).
+Call @kbd{M-x guix} if you prefer to dive in right away
+(@pxref{Keyboard-driven Menu}).
 
 In short, Emacs-Guix provides the following features:
 
@@ -99,8 +99,8 @@ Interfaces for:
 @end itemize
 
 @item
-Magit-like popup interface for all Emacs-Guix @kbd{M-x} commands and
-Guix shell commands (@pxref{Popup Interface}).
+Keyboard-driven magit-like menu for all Emacs-Guix @kbd{M-x} and Guix
+shell commands (@pxref{Keyboard-driven Menu}).
 
 @item
 Modes to view logs of package builds (@pxref{Build Log Mode}).
@@ -185,10 +185,9 @@ features as without Guix.
 interfaces (to display packages, generations, licenses, etc.).
 
 @item
-@uref{https://github.com/magit/magit-popup, magit-popup library}.  You
-already have this library if you use Magit 2.1.0 or later.  This
-library is required only for @kbd{M-x@tie{}guix} command (@pxref{Popup
-Interface}).
+@uref{https://github.com/magit/transient, transient library}.  This
+library is required only for @kbd{M-x@tie{}guix} command
+(@pxref{Keyboard-driven Menu}).
 
 @item
 @uref{https://github.com/Fanael/edit-indirect, edit-indirect library},
@@ -204,7 +203,7 @@ list of packages (@pxref{Package Keys}).
 @end itemize
 
 To sum up, most likely, you'll need all the above dependencies except
-maybe @code{magit-popup}, @code{edit-indirect} and @code{build-farm}.
+maybe @code{transient}, @code{edit-indirect} and @code{build-farm}.
 
 @node Using from Git
 @section Using from Git
@@ -1058,24 +1057,24 @@ Open @file{@dots{}/guix/licenses.scm} and move to the specified license.
 @end table
 
 @c ----------------------------------------------------------------
-@node Popup Interface
-@chapter Popup Interface
+@node Keybord-driven Menu
+@chapter Keybord-driven Menu
 
-If you ever used Magit, you know what ``popup interface'' is
-(@pxref{Top,,, magit-popup, Magit-Popup User Manual}).  Even if you are
-not acquainted with Magit, there should be no worries as it is very
-intuitive.
+Transient is the library used to implement keybord-driven ``menus'' like
+in Magit (@pxref{Top,,, transient, Transient User Manual}). Even if you
+are not acquainted with Magit, this interface it is very intuitive.
 
 @findex guix
-So, @kbd{M-x@tie{}guix} command provides a top-level popup interface
-for almost all the available Emacs-Guix commands.  It has 2 advantages
-comparing with calling @kbd{M-x@tie{}guix-@dots{}} commands directly:
+So, @kbd{M-x@tie{}guix} command provides a top-level keyboard-driven
+menu for almost all the available Emacs-Guix commands.  It has 2
+advantages comparing with calling @kbd{M-x@tie{}guix-@dots{}} commands
+directly:
 
 @itemize
 
 @item
 There is no need to remember the names of Emacs-Guix commands, as you
-can always find them in @kbd{M-x@tie{}guix} and its sub-popups.
+can always find them in @kbd{M-x@tie{}guix} and its suffix transients.
 
 @item
 It is faster (well, if you know what you are going to call), as it may
@@ -1092,17 +1091,17 @@ easy accessible key combination, for example, to @kbd{@key{super}-g}:
 (global-set-key (kbd "s-g") 'guix)
 @end example
 
-@node Guix Popup Interface
-@section Guix Popup Interface
+@node Guix Keyboard-driven Menu
+@section Guix Keyboard-driven Menu
 
 @findex guix-command
-There is one rather special sub-popup in @kbd{M-x@tie{}guix}.  It is
-bind to @kbd{c} by default, and you can call it separately with
-@kbd{M-x@tie{}guix-command}.  It is a popup interface for
-@code{guix@tie{}@dots{}}  shell commands.  It is probably not very
+There is one rather special transient suffix in @kbd{M-x@tie{}guix}.  It
+is bind to @kbd{c} by default, and you can call it separately with
+@kbd{M-x@tie{}guix-command}.  It is a keyboard-driven, magit-like menu
+for @code{guix@tie{}@dots{}} shell commands.  It is probably not very
 useful, as it provides all the options and flags for all the shell
-actions and subcommands, so it may be confusing to see them all at
-once.  Nevertheless, a description of this thing follows.
+actions and subcommands, so it may be confusing to see them all at once.
+Nevertheless, a description of this thing follows.
 
 When you select an option, you'll be prompted for a value in the
 minibuffer.  Many values have completions, so don't hesitate to press
diff --git a/doc/htmlxref.cnf b/doc/htmlxref.cnf
index d547107..b42ac28 100644
--- a/doc/htmlxref.cnf
+++ b/doc/htmlxref.cnf
@@ -4,9 +4,9 @@
 # manuals in the generated HTML pages (created by "make manual"
 # command).  See (info "(texinfo) HTML Xref Configuration") for details.
 
-MAGIT_POPUP = https://magit.vc/manual/magit-popup
-magit-popup     mono    ${MAGIT_POPUP}
-magit-popup     node    ${MAGIT_POPUP}/
+TRANSIENT = https://magit.vc/manual/transient
+transient     mono    ${TRANSIENT}
+transient     node    ${TRANSIENT}/
 
 GEISER = http://www.nongnu.org/geiser
 geiser          mono    ${GEISER}
diff --git a/elisp/guix-command.el b/elisp/guix-command.el
index cbfc0fa..affda4e 100644
--- a/elisp/guix-command.el
+++ b/elisp/guix-command.el
@@ -1,4 +1,4 @@
-;;; guix-command.el --- Popup interface for guix shell commands  -*- lexical-binding: t -*-
+;;; guix-command.el --- Transient interface for guix shell commands  -*- lexical-binding: t -*-
 
 ;; Copyright © 2015–2020 Alex Kost <[email-masked]>
 
@@ -19,16 +19,16 @@
 
 ;;; Commentary:
 
-;; This file provides a magit-like popup interface for guix shell
-;; commands.  You can run a selected command in *shell* buffer, in Guix
-;; REPL, or simply copy it into `kill-ring'.
+;; This file provides a transient interface for guix shell commands.
+;; You can run a selected command in *shell* buffer, in Guix REPL, or
+;; simply copy it into `kill-ring'.
 ;;
 ;; The entry point is "M-x guix-command".  When it is called the first
 ;; time, "guix --help" output is parsed and `guix-COMMAND-action'
 ;; functions are generated for each available guix COMMAND.  Then a
 ;; window with these commands is popped up.  When a particular COMMAND
 ;; is called, "guix COMMAND --help" output is parsed, and a user get a
-;; new popup window with available options for this command and so on.
+;; new transient window with available options for this command and so on.
 
 ;; To avoid hard-coding all guix options, actions, etc., as much data is
 ;; taken from "guix ... --help" outputs as possible.  But this data is
@@ -40,7 +40,7 @@
 ;; structures.
 
 ;; Only "M-x guix-command" is available after this file is loaded.  The
-;; rest commands/actions/popups are generated on the fly only when they
+;; rest commands/actions/transients are generated on the fly only when they
 ;; are needed (that's why there is a couple of `eval'-s in this file).
 
 ;; COMMANDS argument is used by many functions in this file.  It means a
@@ -48,7 +48,7 @@
 ;; ("import" "gnu").  The empty list stands for the plain "guix" without
 ;; subcommands.
 
-;; All actions in popup windows are divided into 2 groups:
+;; All actions in transient windows are divided into 2 groups:
 ;;
 ;; - 'Popup' actions - used to pop up another window.  For example, every
 ;;   action in the 'guix' or 'guix import' window is a popup action.  They
@@ -56,13 +56,13 @@
 ;;
 ;; - 'Execute' actions - used to do something with the command line (to
 ;;   run a command in Guix REPL or to copy it into kill-ring) constructed
-;;   with the current popup.  They are defined by
+;;   with the current transient.  They are defined by
 ;;   `guix-command-define-execute-action' macro.
 
 ;;; Code:
 
 (require 'cl-lib)
-(require 'magit-popup)
+(require 'transient)
 (require 'bui-utils)
 (require 'guix nil t)
 (require 'guix-utils)
@@ -74,7 +74,7 @@
 (require 'guix-external)
 
 (defgroup guix-commands nil
-  "Settings for guix popup windows."
+  "Settings for guix transient windows."
   :group 'guix)
 
 (defvar guix-command-complex-with-shared-arguments
@@ -484,10 +484,10 @@ to be modified."
       argument))
 
 (defun guix-command-improve-arguments (arguments commands)
-  "Return ARGUMENTS for 'guix COMMANDS ...' modified for popup interface."
+  "Return ARGUMENTS for 'guix COMMANDS ...' modified for transient interface."
   (let ((improvers (cons 'guix-command-improve-common-argument
                          (bui-assoc-value guix-command-argument-improvers
-                                           commands))))
+                                          commands))))
     (mapcar (lambda (argument)
               (guix-command-improve-argument argument improvers))
             arguments)))
@@ -592,7 +592,7 @@ commands.")
   "Return additional arguments for COMMANDS."
   (let ((rest-arg (guix-command-rest-argument commands)))
     (append (bui-assoc-value guix-command-additional-arguments
-                              commands)
+                             commands)
             (and rest-arg (list rest-arg)))))
 
 ;; Ideally, only `guix-command-all-arguments' function should exist with
@@ -611,7 +611,7 @@ commands.")
 
 (defun guix-command-all-arguments (&optional commands)
   ;; Note: `guix-command-arguments' name cannot be used because function
-  ;; with this name is generated by `magit-define-popup'.
+  ;; with this name is generated by `transient-define-prefix'.
   "Return list of arguments for 'guix COMMANDS ...'."
   (let ((command (car commands)))
     (if (member command
@@ -634,28 +634,25 @@ commands.")
                    (guix-command--all-arguments-memoize (list command))))
       (guix-command--all-arguments commands))))
 
-(defun guix-command-switch->popup-switch (switch)
-  "Return popup switch from command SWITCH argument."
-  (list (guix-command-argument-char switch)
+(defun guix-command-switch->transient-switch (switch)
+  "Return transient switch from command SWITCH argument."
+  (list (format "-%c" (guix-command-argument-char switch))
+        (guix-command-argument-name switch)
         (or (guix-command-argument-doc switch)
-            "Unknown")
-        (guix-command-argument-name switch)))
+            "Unknown")))
 
-(defun guix-command-option->popup-option (option)
-  "Return popup option from command OPTION argument."
-  (list (guix-command-argument-char option)
+(defun guix-command-option->transient-option (option)
+  "Return transient option from command OPTION argument."
+  (list (format "-%c" (guix-command-argument-char option))
+        (guix-command-argument-name option)
         (or (guix-command-argument-doc option)
             "Unknown")
-        (let ((name (guix-command-argument-name option)))
-          (if (string-match-p " \\'" name) ; ends with space
-              name
-            (concat name "=")))
         (or (guix-command-argument-fun option)
             'read-from-minibuffer)))
 
-(defun guix-command-action->popup-action (action)
-  "Return popup action from command ACTION argument."
-  (list (guix-command-argument-char action)
+(defun guix-command-action->transient-suffix (action)
+  "Return transient suffix from command ACTION argument."
+  (list (format "-%c" (guix-command-argument-char action))
         (or (guix-command-argument-doc action)
             (guix-command-argument-name action)
             "Unknown")
@@ -683,8 +680,8 @@ commands.")
   "Return actions from ARGUMENTS."
   (cl-remove-if-not #'guix-command-argument-action? arguments))
 
-
-;;; Post processing popup arguments
+
+;;; Post processing transient arguments
 
 (defvar guix-command-post-processors
   '(("environment"
@@ -698,7 +695,7 @@ commands.")
     ("system"
      guix-command-post-process-rest-single))
   "Alist of guix commands and functions for post-processing
-a list of arguments returned from popup interface.
+a list of arguments returned from transient interface.
 Each function is called on the returned arguments in turn.")
 
 (defvar guix-command-rest-arg-regexp
@@ -760,13 +757,13 @@ Leave '--' string as a separate argument."
    :split? t))
 
 (defun guix-command-post-process-package-args (args)
-  "Adjust popup ARGS for 'guix package' command."
+  "Adjust transient ARGS for 'guix package' command."
   (guix-command-post-process-matching-args
    args (rx string-start (or "--install " "--remove ") (+ any))
    :split? t))
 
 (defun guix-command-post-process-environment-packages (args)
-  "Adjust popup ARGS for specified packages of 'guix environment'
+  "Adjust transient ARGS for specified packages of 'guix environment'
 command."
   (guix-command-post-process-matching-args
    args (rx string-start "++packages " (group (+ any)))
@@ -774,14 +771,14 @@ command."
    :split? t))
 
 (defun guix-command-post-process-environment-ad-hoc (args)
-  "Adjust popup ARGS for '--ad-hoc' argument of 'guix environment'
+  "Adjust transient ARGS for '--ad-hoc' argument of 'guix environment'
 command."
   (guix-command-post-process-matching-args
    args (rx string-start "--ad-hoc " (+ any))
    :split? t))
 
 (defun guix-command-post-process-args (commands args)
-  "Adjust popup ARGS for guix COMMANDS."
+  "Adjust transient ARGS for guix COMMANDS."
   (let* ((command (car commands))
          (processors
           (append (bui-assoc-value guix-command-post-processors commands)
@@ -931,32 +928,31 @@ open the log file(s)."
 ;;; Generating popups, actions, etc.
 
 (defmacro guix-command-define-popup-action (name &optional commands)
-  "Define NAME function to generate (if needed) and run popup for COMMANDS."
+  "Define NAME function to generate (if needed) and run transient for COMMANDS."
   (declare (indent 1) (debug t))
-  (let* ((popup-fun (guix-command-symbol `(,@commands "popup")))
+  (let* ((prefix-fun (guix-command-symbol `(,@commands "prefix")))
          (doc (format "Call `%s' (generate it if needed)."
-                      popup-fun)))
+                      prefix-fun)))
     `(defun ,name (&optional arg)
        ,doc
        (interactive "P")
-       (unless (fboundp ',popup-fun)
-         (guix-command-generate-popup ',popup-fun ',commands))
-       (,popup-fun arg))))
+       (unless (fboundp ',prefix-fun)
+         (guix-command-generate-prefix ',prefix-fun ',commands))
+       (,prefix-fun))))
 
 (defmacro guix-command-define-execute-action (name executor
                                                    &optional commands)
   "Define NAME function to execute the current action for guix COMMANDS.
 EXECUTOR function is called with the current command line arguments."
   (declare (indent 1) (debug t))
-  (let* ((arguments-fun (guix-command-symbol `(,@commands "arguments")))
-         (doc (format "Call `%s' with the current popup arguments."
+  (let* ((doc (format "Call `%s' with the current transient arguments."
                       executor)))
     `(defun ,name (&rest args)
        ,doc
-       (interactive (,arguments-fun))
+       (interactive)
        (,executor (append ',commands
                           (guix-command-post-process-args
-                           ',commands args))))))
+                           ',commands (transient-args (transient-current-command))))))))
 
 (defun guix-command-generate-popup-actions (actions &optional commands)
   "Generate 'popup' commands from ACTIONS arguments for guix COMMANDS."
@@ -977,11 +973,11 @@ EXECUTOR function is called with the current command line arguments."
                    commands (guix-command-argument-name action))
                  ,commands))))))
 
-(defun guix-command-generate-popup (name &optional commands)
-  "Define NAME popup with 'guix COMMANDS ...' interface."
+(defun guix-command-generate-prefix (name &optional commands)
+  "Define NAME prefix with 'guix COMMANDS ...' interface."
   (let* ((command  (car commands))
          (man-page (concat "guix" (and command (concat "-" command))))
-         (doc      (format "Popup window for '%s' command."
+         (doc      (format "Transient for '%s' command."
                            (guix-concat-strings (cons "guix" commands)
                                                 " ")))
          (args     (guix-command-all-arguments commands))
@@ -998,18 +994,22 @@ EXECUTOR function is called with the current command line arguments."
         (guix-command-generate-popup-actions popup-actions commands)
       (guix-command-generate-execute-actions execute-actions commands))
     (eval
-     `(magit-define-popup ,name
+     `(transient-define-prefix ,name ()
         ,doc
-        'guix-commands
         :man-page ,man-page
-        :switches ',(mapcar #'guix-command-switch->popup-switch switches)
-        :options  ',(mapcar #'guix-command-option->popup-option options)
-        :actions  ',(mapcar #'guix-command-action->popup-action actions)
-        :max-action-columns 4))))
+        ,@(and switches
+               `(["Switches"
+                  ,@(mapcar #'guix-command-switch->transient-switch switches)]))
+        ,@(and options
+               `(["Options"
+                  ,@(mapcar #'guix-command-option->transient-option options)]))
+        ,@(and actions
+               `(["Actions"
+                  ,@(mapcar #'guix-command-action->transient-suffix actions)]))))))
 
-(declare-function guix-command-popup "guix-command" t)
+(declare-function guix-command-prefix "guix-command" t)
 
-;;;###autoload (autoload 'guix-command "guix-command" "Popup window for 'guix' shell commands." t)
+;;;###autoload (autoload 'guix-command "guix-command" "Transient for 'guix' shell commands." t)
 (guix-command-define-popup-action guix-command)
 
 (declare-function guix-find-package-definition "guix-package" t)
diff --git a/elisp/guix-help.el b/elisp/guix-help.el
index da69b09..d4aa472 100644
--- a/elisp/guix-help.el
+++ b/elisp/guix-help.el
@@ -68,7 +68,7 @@ If ARG is non-nil (interactively with prefix), show Guix info manual."
   :group 'guix-help-faces)
 
 (defvar guix-help-specifications
-  '("Popup interface for the rest commands"
+  '("Transient interface for the rest commands"
     guix
     "Show packages and their definitions"
     guix-all-packages
diff --git a/elisp/guix-popup.el b/elisp/guix-popup.el
deleted file mode 100644
index 77728f6..0000000
--- a/elisp/guix-popup.el
+++ /dev/null
@@ -1,227 +0,0 @@
-;;; guix-popup.el --- Popup interface for Emacs-Guix commands
-
-;; Copyright © 2018–2019, 2021 Alex Kost <[email-masked]>
-
-;; This file is part of Emacs-Guix.
-
-;; Emacs-Guix is free software; you can redistribute it and/or modify
-;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-;;
-;; Emacs-Guix is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-;; GNU General Public License for more details.
-;;
-;; You should have received a copy of the GNU General Public License
-;; along with Emacs-Guix.  If not, see <http://www.gnu.org/licenses/>.
-
-;;; Commentary:
-
-;; This file provides popup interface (using `magit-popup' library) for
-;; Emacs-Guix commands.
-
-;;; Code:
-
-(require 'magit-popup)
-(require 'guix-profiles)
-
-(defgroup guix-popup nil
-  "Popup interface for Emacs-Guix commands."
-  :group 'guix)
-
-;;;###autoload (autoload 'guix-popup "guix-popup" nil t)
-(magit-define-popup guix-popup
-  "Show popup buffer for Emacs-Guix commands."
-  'guix-popup
-  :actions '("Sub-popups"
-             (?p "packages" guix-package-popup)
-             (?P "profiles" guix-profile-popup)
-             (?s "services" guix-service-popup)
-             (?y "system commands" guix-system-popup)
-             (?l "package licenses" guix-license-popup)
-             (?S "store" guix-store-popup)
-             (?m "major/minor modes" guix-mode-popup)
-             (?c "guix shell commands" guix-command)
-             "Miscellaneous commands"
-             (?H "calculate file hash" guix-hash)
-             (?E "set Emacs environment" guix-set-emacs-environment)
-             "Auxiliary commands"
-             (?a "about" guix-about)
-             (?h "help (\"refcard\")" guix-help)
-             (?i "info manual" guix-info)
-             (?v "version" guix-version)
-             (?b "switch to buffer" guix-switch-to-buffer)
-             (?B "report guix bug" guix-report-bug))
-  :max-action-columns #'guix-popup-max-columns)
-
-(defun guix-popup-max-columns (heading)
-  "Return the number of `:max-action-columns' for HEADING.
-This function is used by command `guix-popup'."
-  (pcase heading
-    ("Sub-popups" 1)
-    ("Miscellaneous commands" 1)
-    (_ 2)))
-
-;;;###autoload
-(defalias 'guix #'guix-popup
-  "Popup interface for Emacs-Guix commands.")
-
-(defun guix-popup-variable-value (var-name)
-  "Return string formatted for popup buffer.
-String is made of variable VAR-NAME and its value."
-  (concat (propertize (symbol-name var-name)
-                      'face font-lock-variable-name-face)
-          " "
-          (propertize (prin1-to-string (symbol-value var-name))
-                      'face 'magit-popup-option-value)))
-
-(defun guix-popup-format-profile ()
-  "Return profile, formatted for '\\[guix-popup]'."
-  (guix-popup-variable-value 'guix-current-profile))
-
-(defvar guix-popup-profile-variable
-  '(?p "profile"
-       guix-set-current-profile
-       guix-popup-format-profile)
-  "Popup structure for variable `guix-current-profile'.")
-
-
-;;; Sub-popups
-
-(magit-define-popup guix-package-popup
-  "Show popup buffer for package commands."
-  'guix-package-popup
-  :variables (list guix-popup-profile-variable)
-  :actions '("Show packages"
-             (?a "all" guix-all-packages)
-             (?i "installed" guix-installed-packages)
-             (?o "obsolete" guix-obsolete-packages)
-             (?s "superseded" guix-superseded-packages)
-             (?h "hidden" guix-hidden-packages)
-             "Search for packages"
-             (?n "by name" guix-packages-by-name)
-             (?N "by regexp (in name only)" guix-packages-by-name-regexp)
-             (?r "by regexp (in name, synopsis, description)"
-                 guix-packages-by-regexp)
-             (?L "by location" guix-packages-by-location)
-             (?c "by license" guix-packages-by-license)
-             (?d "depending on other package(s)" guix-dependent-packages)
-             (?f "packages from file" guix-package-from-file)
-             (?y "packages from system config file"
-                 guix-packages-from-system-config-file)
-             "Package locations"
-             (?l "show package locations" guix-package-locations)
-             (?e "\"edit\" package (find package definition)"
-                 guix-find-package-definition)
-             (?F "find location file" guix-find-package-location-file)
-             "Other commands"
-             (?g "package graph" guix-package-graph)
-             (?z "package size" guix-package-size)
-             (?t "package lint" guix-package-lint)
-             (?C "lint checkers" guix-lint-checkers)
-             (?T "total number of packages" guix-number-of-packages))
-  :max-action-columns #'guix-package-popup-max-columns)
-
-(defun guix-package-popup-max-columns (heading)
-  "Return the number of `:max-action-columns' for HEADING.
-This function is used by command `guix-package-popup'."
-  (pcase heading
-    ("Show packages" 2)
-    ("Other commands" 2)
-    (_ 1)))
-
-(magit-define-popup guix-profile-popup
-  "Show popup buffer for profiles and generations commands."
-  'guix-profile-popup
-  :variables (list guix-popup-profile-variable)
-  :actions '("Show profiles"
-             (?a "all" guix-profiles)
-             (?s "system" guix-system-profile)
-	     (?h "home" guix-home-profile)
-             (?c "current" guix-current-profile)
-             "Show generations (of the current profile)"
-             (?g "all" guix-generations)
-             (?t "by time" guix-generations-by-time)
-             (?l "last" guix-last-generations)
-             "Other commands"
-             (?M "apply manifest to the current profile"
-                 guix-apply-manifest))
-  :max-action-columns 1)
-
-(magit-define-popup guix-service-popup
-  "Show popup buffer for service commands."
-  'guix-service-popup
-  :actions '("Show services"
-             (?a "all system services" guix-all-services)
-             (?h "all Home services" guix-all-home-services)
-             (?d "default" guix-default-services)
-             (?n "by name" guix-services-by-name)
-             (?r "by regexp" guix-services-by-regexp)
-             (?L "by location" guix-services-by-location)
-             (?y "services from system config file"
-                 guix-services-from-system-config-file)
-             "Service locations"
-             (?l "show service locations" guix-service-locations)
-             (?e "\"edit\" service (find service definition)"
-                 guix-find-service-definition)
-             (?F "find location file" guix-find-service-location-file))
-  :max-action-columns 1)
-
-(magit-define-popup guix-system-popup
-  "Show popup buffer for system commands."
-  'guix-system-popup
-  :actions '("From system profile"
-             (?p "packages" guix-installed-system-packages)
-             (?P "profile" guix-system-profile)
-             (?g "all generations" guix-system-generations)
-             (?t "generations by time" guix-system-generations-by-time)
-             (?l "last generations" guix-last-system-generations)
-             "From system configuration file"
-             (?y "system" guix-system-from-file)
-             (?k "packages" guix-packages-from-system-config-file)
-             (?s "services" guix-services-from-system-config-file))
-  :max-action-columns 1)
-
-(magit-define-popup guix-license-popup
-  "Show popup buffer for license commands."
-  'guix-license-popup
-  :actions '((?a "show all package licenses" guix-licenses)
-             (?u "browse license URL" guix-browse-license-url)
-             (?e "\"edit\" license (find license definition)"
-                 guix-find-license-definition)
-             (?F "find license location file"
-                 guix-find-license-location-file))
-  :max-action-columns 1)
-
-(magit-define-popup guix-store-popup
-  "Show popup buffer for store commands."
-  'guix-store-popup
-  :actions '("Show store items"
-             (?l "live items" guix-store-live-items)
-             (?d "dead items" guix-store-dead-items)
-             (?e "failures" guix-store-failures)
-             (?i "single item" guix-store-item)
-             (?D "derivers" guix-store-item-derivers)
-             (?R "requisites" guix-store-item-requisites)
-             (?f "referrers" guix-store-item-referrers)
-             (?F "references" guix-store-item-references))
-  :max-action-columns 2)
-
-(magit-define-popup guix-mode-popup
-  "Show popup buffer for Emacs-Guix major/minor modes."
-  'guix-mode-popup
-  :actions '("Modes"
-             (?p "guix-prettify-mode" guix-prettify-mode)
-             (?P "global-guix-prettify-mode" global-guix-prettify-mode)
-             (?b "guix-build-log-minor-mode" guix-build-log-minor-mode)
-             (?B "guix-build-log-mode" guix-build-log-mode)
-             (?d "guix-devel-mode" guix-devel-mode)
-             (?D "guix-derivation-mode" guix-derivation-mode)
-             (?e "guix-env-var-mode" guix-env-var-mode))
-  :max-action-columns 1)
-
-(provide 'guix-popup)
-
-;;; guix-popup.el ends here
diff --git a/elisp/guix-transient.el b/elisp/guix-transient.el
new file mode 100644
index 0000000..36a4733
--- /dev/null
+++ b/elisp/guix-transient.el
@@ -0,0 +1,187 @@
+;;; guix-transient.el --- Transient interface for Emacs-Guix commands
+
+;; Copyright © 2018–2019, 2021 Alex Kost <[email-masked]>
+;; Copyright © 2025 Nicolas Graves <[email-masked]>
+
+;; This file is part of Emacs-Guix.
+
+;; Emacs-Guix is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Emacs-Guix is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Emacs-Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file provides transient interface (using `transient' library) for
+;; Emacs-Guix commands.
+
+;;; Code:
+
+(require 'transient)
+(require 'guix-profiles)
+
+(defgroup guix-transient nil
+  "Transient interface for Emacs-Guix commands."
+  :group 'guix)
+
+(defun guix-transient-format-profile ()
+  "Return profile, formatted for `\\[guix-transient]'."
+  (interactive)
+  (concat (propertize "profile"
+                      'face 'transient-heading)
+          " "
+          (propertize (symbol-value 'guix-current-profile)
+                      'face 'transient-value)))
+
+(transient-define-infix guix-set-profile-infix ()
+  :class 'transient-lisp-variable
+  :variable 'guix-current-profile
+  :reader 'guix-set-current-profile
+  :description "Set profile")
+
+;;;###autoload
+(transient-define-prefix guix-transient ()
+  "Show transient interface for Emacs-Guix commands."
+  [["Sub-menus"
+    ("p" "packages" guix-package-transient)
+    ("P" "profiles" guix-profile-transient)
+    ("s" "services" guix-service-transient)
+    ("y" "system commands" guix-system-transient)
+    ("l" "package licenses" guix-license-transient)
+    ("S" "store" guix-store-transient)
+    ("m" "major/minor modes" guix-mode-transient)
+    ("c" "guix shell commands" guix-command)]
+   ["Miscellaneous commands"
+    ("H" "calculate file hash" guix-hash)
+    ("E" "set Emacs environment" guix-set-emacs-environment)]
+   ["Auxiliary commands"
+    ("a" "about" guix-about)
+    ("h" "help (\"refcard\")" guix-help)
+    ("i" "info manual" guix-info)
+    ("v" "version" guix-version)
+    ("b" "switch to buffer" guix-switch-to-buffer)
+    ("B" "report guix bug" guix-report-bug)]])
+
+;;;###autoload
+(defalias 'guix #'guix-transient
+  "Transient interface for Emacs-Guix commands.")
+
+(transient-define-prefix guix-package-transient ()
+  "Show transient interface for package commands."
+  ["Profile"
+   ("p" "Set profile" guix-set-profile-infix)
+   ("v" "Current profile" guix-transient-format-profile :transient nil :if (lambda () guix-current-profile))]
+  ["Show packages"
+   ("a" "all" guix-all-packages)
+   ("i" "installed" guix-installed-packages)
+   ("o" "obsolete" guix-obsolete-packages)
+   ("s" "superseded" guix-superseded-packages)
+   ("h" "hidden" guix-hidden-packages)]
+  ["Search for packages"
+   ("n" "by name" guix-packages-by-name)
+   ("N" "by regexp (in name only)" guix-packages-by-name-regexp)
+   ("r" "by regexp (in name, synopsis, description)" guix-packages-by-regexp)
+   ("L" "by location" guix-packages-by-location)
+   ("c" "by license" guix-packages-by-license)
+   ("d" "depending on other package(s)" guix-dependent-packages)
+   ("f" "packages from file" guix-package-from-file)
+   ("y" "packages from system config file" guix-packages-from-system-config-file)]
+  ["Package locations"
+   ("l" "show package locations" guix-package-locations)
+   ("e" "\"edit\" package (find package definition)" guix-find-package-definition)
+   ("F" "find location file" guix-find-package-location-file)]
+  ["Other commands"
+   ("g" "package graph" guix-package-graph)
+   ("z" "package size" guix-package-size)
+   ("t" "package lint" guix-package-lint)
+   ("C" "lint checkers" guix-lint-checkers)
+   ("T" "total number of packages" guix-number-of-packages)])
+
+(transient-define-prefix guix-profile-transient ()
+  "Show transient interface for profiles and generations commands."
+  ["Profile"
+   ("p" "Set profile" guix-set-profile-infix)
+   ("v" "Current profile" guix-transient-format-profile :transient nil :if (lambda () guix-current-profile))]
+  ["Show profiles"
+   ("a" "all" guix-profiles)
+   ("s" "system" guix-system-profile)
+   ("h" "home" guix-home-profile)
+   ("c" "current" guix-current-profile)]
+  ["Show generations (of the current profile)"
+   ("g" "all" guix-generations)
+   ("t" "by time" guix-generations-by-time)
+   ("l" "last" guix-last-generations)]
+  ["Other commands"
+   ("M" "apply manifest to the current profile" guix-apply-manifest)])
+
+(transient-define-prefix guix-service-transient ()
+  "Show transient interface for service commands."
+  [["Show services"
+    ("a" "all system services" guix-all-services)
+    ("h" "all Home services" guix-all-home-services)
+    ("d" "default" guix-default-services)
+    ("n" "by name" guix-services-by-name)
+    ("r" "by regexp" guix-services-by-regexp)
+    ("L" "by location" guix-services-by-location)
+    ("y" "services from system config file" guix-services-from-system-config-file)]
+   ["Service locations"
+    ("l" "show service locations" guix-service-locations)
+    ("e" "\"edit\" service (find service definition)" guix-find-service-definition)
+    ("F" "find location file" guix-find-service-location-file)]])
+
+(transient-define-prefix guix-system-transient ()
+  "Show transient interface for system commands."
+  [["From system profile"
+    ("p" "packages" guix-installed-system-packages)
+    ("P" "profile" guix-system-profile)
+    ("g" "all generations" guix-system-generations)
+    ("t" "generations by time" guix-system-generations-by-time)
+    ("l" "last generations" guix-last-system-generations)]
+   ["From system configuration file"
+    ("y" "system" guix-system-from-file)
+    ("k" "packages" guix-packages-from-system-config-file)
+    ("s" "services" guix-services-from-system-config-file)]])
+
+(transient-define-prefix guix-license-transient ()
+  "Show transient interface for license commands."
+  [["License commands"
+    ("a" "show all package licenses" guix-licenses)
+    ("u" "browse license URL" guix-browse-license-url)
+    ("e" "\"edit\" license (find license definition)" guix-find-license-definition)
+    ("F" "find license location file" guix-find-license-location-file)]])
+
+(transient-define-prefix guix-store-transient ()
+  "Show transient interface for store commands."
+  [["Show store items"
+    ("l" "live items" guix-store-live-items)
+    ("d" "dead items" guix-store-dead-items)
+    ("e" "failures" guix-store-failures)
+    ("i" "single item" guix-store-item)]
+   ["Show details"
+    ("D" "derivers" guix-store-item-derivers)
+    ("R" "requisites" guix-store-item-requisites)
+    ("f" "referrers" guix-store-item-referrers)
+    ("F" "references" guix-store-item-references)]])
+
+(transient-define-prefix guix-mode-transient ()
+  "Show transient interface for Emacs-Guix major/minor modes."
+  [["Modes"
+    ("p" "guix-prettify-mode" guix-prettify-mode)
+    ("P" "global-guix-prettify-mode" global-guix-prettify-mode)
+    ("b" "guix-build-log-minor-mode" guix-build-log-minor-mode)
+    ("B" "guix-build-log-mode" guix-build-log-mode)
+    ("d" "guix-devel-mode" guix-devel-mode)
+    ("D" "guix-derivation-mode" guix-derivation-mode)
+    ("e" "guix-env-var-mode" guix-env-var-mode)]])
+
+(provide 'guix-transient)
+
+;;; guix-transient.el ends here
diff --git a/elisp/guix.el b/elisp/guix.el
index b4c170a..2686a24 100644
--- a/elisp/guix.el
+++ b/elisp/guix.el
@@ -6,7 +6,7 @@
 ;; Version: 0.5.2
 ;; URL: https://emacs-guix.gitlab.io/website/
 ;; Keywords: tools
-;; Package-Requires: ((emacs "24.3") (dash "2.11.0") (geiser "0.8") (bui "1.2.0") (magit-popup "2.1.0") (edit-indirect "0.1.4"))
+;; Package-Requires: ((emacs "24.3") (dash "2.11.0") (geiser "0.8") (bui "1.2.0") (transient "0.8.4") (edit-indirect "0.1.4"))
 
 ;; This file is part of Emacs-Guix.
 
diff --git a/elisp/local.mk b/elisp/local.mk
index 3bc0a27..4efbae5 100644
--- a/elisp/local.mk
+++ b/elisp/local.mk
@@ -40,8 +40,8 @@ if EDITINDIRECT_DIR
   AM_ELCFLAGS += -L "$(editindirectlispdir)"
 endif
 
-if POPUP_DIR
-  AM_ELCFLAGS += -L "$(popuplispdir)"
+if TRANSIENT_DIR
+  AM_ELCFLAGS += -L "$(transientlispdir)"
 endif
 
 if EMACS_Q
@@ -81,7 +81,7 @@ EL_FILES =					\
   %D%/guix-service.el				\
   %D%/guix-pcomplete.el				\
   %D%/guix-prettify.el				\
-  %D%/guix-popup.el				\
+  %D%/guix-transient.el				\
   %D%/guix-ui-messages.el			\
   %D%/guix-ui.el				\
   %D%/guix-ui-license.el			\
-- 
2.48.1