transient
transient copied to clipboard
Add some examples of basic transients and generally improve the documentation
Hi! First of all, thank you very much for your work on this package and Magit in general!
I have recently started working on a package which uses transient to provide a user interface to the rust cli utility called cargo
. Transient is a pleasure to work with and the manual is very detailed but I think that it could benefit from some basic examples of Transient being used(defining infix command, creating a basic transient and so on...). If you are interested in adding something such as this, I have no problem contributing some code.
Looking around, it seems like it would be perfect to put this in the wiki for this package and perhaps link to it from the manual or maybe even just adding these pieces into the manual itself. Either way, if you are open to adding some examples, then please let me know and I will create some and contribute them. I have not worked with every feature that transient provides but whatever I can offer will hopefully be helpful to somebody else.
I plan to write some tutorial soonish.
Of course it you feel like it, there's nothing to keep you from doing the same.
This is crucial. I love Magit, and I love its interface. And I'd love to use transient as a front end for a simple query form that sets some options, sets various optional search strings and configurations, and then processes the query request and consumes the returned results. Basically avoid a clunky widget interface. Just like Magit does.
So, I spent some time with the extensive transient documentation, and, while well-written, it reads more like a formal resource specification document than a usage manual for potential users. There is so much assumed domain knowledge on each and every info topic page that isn't further referenced or expanded upon.
Here's an example:
I've been trying to figure out how set an infix "possibly by reading a new value in the minibuffer" as mentioned as possible in the first paragraph of the docs. For example, my interface might allow the user to specify "Author: ___________" and fill in one or more authors to search.
So I have a look at the help on define-transient-command
: the front door to this whole amazing kingdom. No mention of text based infixes there, but perhaps GROUPS is the ticket, since that seems to be the way to group and specify those infix commands. There I learn that groups are vectors. OK, what is in those vectors? Some boilerplate, now drilling down the good stuff... ELEMENTS
. OK, that's where the real action clearly is. An ELEMENT
can be a sub-group, or a "mixture of lists that specify commands and strings". Hmm... still no actionable info for building a new transient. What next? At the very end of this page, I learn that suffix specification is in the next node. OK, looking at that page.
Here I finally learn that "an infix is a special kind of suffix". Useful. I learn that I can use keywords which are :class
or a keyword supported by that class' constructor. But I don't know which :class
's would even be appropriate or useful. I can sort of infer from the along-the-way information above that transient-option
and transient-switch
are some possible classes. But they are not linked or further referenced. So I do a full text search on those terms and finally find the "Suffix Classes" doc page. There I learn about transient-variables
as appropriate for "Classes used for infix commands that represent variables". This sounds promising. I still don't know what these classes are really for. But alas, it's not further documented. I end my search in vain.
I really don't mean this to be offensive or critical. It's abundantly clear that you put a huge amount of time into making this interface flexible, adaptable, extensible, and highly capable. It's just that for us mere mortals who hope to use it, the level of implicit knowledge needed is a huge stumbling block. I worry that many potential users will get bogged down as I did and just give up. Obviously re-writing docs with less implicit domain knowledge would be a big effort, but I think some rich examples (with screenshots!) covering all the useful cases would be highly highly valuable, and could serve as an effective on-ramp.
Thanks for this (and Magit!).
It's also not very clear when one should use define-suffix-command
versus using a plain function. For example:
(define-transient-command magit-pull ()
"Pull from another repository."
:man-page "git-pull"
[:description
(lambda () (if magit-pull-or-fetch "Pull arguments" "Arguments"))
("-r" "Rebase local commits" ("-r" "--rebase"))]
[:description
(lambda ()
(if-let ((branch (magit-get-current-branch)))
(concat
(propertize "Pull into " 'face 'transient-heading)
(propertize branch 'face 'magit-branch-local)
(propertize " from" 'face 'transient-heading))
(propertize "Pull from" 'face 'transient-heading)))
("p" magit-pull-from-pushremote)
("u" magit-pull-from-upstream)
("e" "elsewhere" magit-pull-branch)]
["Fetch from"
:if-non-nil magit-pull-or-fetch
("f" "remotes" magit-fetch-all-no-prune)
("F" "remotes and prune" magit-fetch-all-prune)]
["Fetch"
:if-non-nil magit-pull-or-fetch
("o" "another branch" magit-fetch-branch)
("s" "explicit refspec" magit-fetch-refspec)
("m" "submodules" magit-fetch-modules)]
["Configure"
("r" magit-branch.<branch>.rebase :if magit-get-current-branch)
("C" "variables..." magit-branch-configure)]
(interactive)
(transient-setup 'magit-pull nil nil :scope (magit-get-current-branch)))
(define-suffix-command magit-pull-from-pushremote (args)
"Pull from the push-remote of the current branch.
When the push-remote is not configured, then read the push-remote
from the user, set it, and then pull from it. With a prefix
argument the push-remote can be changed before pulling from it."
:if 'magit-get-current-branch
:description 'magit-pull--pushbranch-description
(interactive (list (magit-pull-arguments)))
(pcase-let ((`(,branch ,remote)
(magit--select-push-remote "pull from there")))
(run-hooks 'magit-credential-hook)
(magit-run-git-async "pull" args remote branch)))
(defun magit-fetch-branch (remote branch args)
"Fetch a BRANCH from a REMOTE."
(interactive
(let ((remote (magit-read-remote-or-url "Fetch from remote or url")))
(list remote
(magit-read-remote-branch "Fetch branch" remote)
(magit-fetch-arguments))))
(magit-git-fetch remote (cons branch args)))
I guess that magit-pull-from-pushremote
is defined using define-suffix-command
because it allows for the :if
keyword to avoid having to duplicate logic, but it'd be nice if there was some kind of "rules of thumb".
It's also not very clear when one should use
define-suffix-command
versus using a plain function.
Yes. Only certain special combinations can be defined "in-line" using define-transient-command
. For these, you can use strings which gets turned into a command, but for others it requires a real command symbol. For example, I've not been able to define a transient-option
inline with :multi-value. Maybe the suffix/infix parser could be improved to permit more inline definitions. What's hard is that, e.g., in:
[:description "Options"
(db-switch-1)
(db-switch-2)]
Those names db-switch-1
must be defined symbols with a function definition or alias. You can't for example have them be functions which can be called to return an anonymous function. You'd need a definite-infix-argument
that returns a function without aliasing it for that as well. Useful for cutting down on the boilerplate. In principle you can define other derived classes to do this, but I haven't had much success with that, in part because by default only very few (one?) type of infix is supported to be defined inline.
I'm also lost. All I want to do is define an infix command that sets a variable in the source buffer. It seems like a simple enough task, and something much like what Magit infix commands do which set Git variables. This is even mentioned in the Transient manual:
-- Macro: define-infix-argument name arglist [docstring] [keyword
value]...
This macro defines NAME as a transient infix command.
It is an alias for ‘define-infix-command’. Only use this alias to
define an infix command that actually sets an infix argument. To
define an infix command that, for example, sets a variable, use
‘define-infix-command’ instead.
So it says that I should use define-infix-command
. But the examples I see in Magit seem to use define-infix-argument
, so let's start with that. Here's my code:
(defclass org-ql-view--variable (transient-variable)
((ignored :initarg :ignored)))
(cl-defmethod transient-infix-set ((obj org-ql-view--variable) value)
"Set an Org QL View variable. FIXME"
(let ((variable (oref obj variable)))
(oset obj value value)
(set (make-local-variable (oref obj variable)) value)
(unless (or value transient--prefix)
(message "Unset %s" variable))))
(define-transient-command org-ql-view-edit ()
"Edit the current Org QL view."
["Query"
(org-ql-view:--query :description "Edit query")]
[["View"
("g" "Refresh" org-ql-view-refresh)
("s" "Save" org-ql-view-save)]])
(define-infix-argument org-ql-view:--query ()
;; :description "Edit query"
:class 'org-ql-view--variable
:key "-q"
:argument "--query="
:variable 'org-ql-view-query
:prompt "Query: "
:reader (lambda (prompt _initial-input history)
;; FIXME: Figure out how to integrate initial-input.
(read-string prompt (when org-ql-view-query
(format "%S" org-ql-view-query))
history)))
So here are the steps I follow, as a user:
-
M-x org-ql-view-edit RET
. This shows the Transient buffer. -
-q
. This causes awrong-type-argument
error:
Debugger entered--Lisp error: (wrong-type-argument (or eieio-object class) nil obj)
signal(wrong-type-argument ((or eieio-object class) nil obj))
#f(compiled-function (obj slot) "Return the value in OBJ at SLOT in the object vector." #<bytecode 0x3501b1>)(nil command)
eieio-oref--closql-oref(#f(compiled-function (obj slot) "Return the value in OBJ at SLOT in the object vector." #<bytecode 0x3501b1>) nil command)
apply(eieio-oref--closql-oref #f(compiled-function (obj slot) "Return the value in OBJ at SLOT in the object vector." #<bytecode 0x3501b1>) (nil command))
eieio-oref(nil command)
(symbol-name (eieio-oref transient--prefix (quote command)))
(setq mode-line-buffer-identification (symbol-name (eieio-oref transient--prefix (quote command))))
(progn (select-window (car save-selected-window--state) (quote norecord)) (if transient-enable-popup-navigation (progn (setq focus (button-get (point) (quote command))))) (erase-buffer) (set-window-hscroll transient--window 0) (set-window-dedicated-p transient--window t) (set-window-parameter transient--window (quote no-other-window) t) (setq window-size-fixed t) (if (and (boundp (quote tab-line-format)) tab-line-format) (progn (setq tab-line-format nil))) (setq mode-line-format (if (eq transient-mode-line-format (quote line)) nil transient-mode-line-format)) (setq mode-line-buffer-identification (symbol-name (eieio-oref transient--prefix (quote command)))) (if transient-enable-popup-navigation (set (make-local-variable (quote cursor-in-non-selected-windows)) (quote box)) (setq cursor-type nil)) (setq display-line-numbers nil) (setq show-trailing-whitespace nil) (transient--insert-groups) (if (or transient--helpp transient--editp) (progn (transient--insert-help))) (if (eq transient-mode-line-format (quote line)) (progn (insert (propertize "__" (quote face) (quote transient-separator) (quote display) (quote (space :height (1))))) (insert (propertize "\n" (quote face) (quote transient-separator) (quote line-height) t)))) (let ((window-resize-pixelwise t) (window-size-fixed nil)) (fit-window-to-buffer nil nil 1)) (goto-char (point-min)) (if transient-force-fixed-pitch (progn (transient--force-fixed-pitch))) (if transient-enable-popup-navigation (progn (transient--goto-button focus))))
(unwind-protect (progn (select-window (car save-selected-window--state) (quote norecord)) (if transient-enable-popup-navigation (progn (setq focus (button-get (point) (quote command))))) (erase-buffer) (set-window-hscroll transient--window 0) (set-window-dedicated-p transient--window t) (set-window-parameter transient--window (quote no-other-window) t) (setq window-size-fixed t) (if (and (boundp (quote tab-line-format)) tab-line-format) (progn (setq tab-line-format nil))) (setq mode-line-format (if (eq transient-mode-line-format (quote line)) nil transient-mode-line-format)) (setq mode-line-buffer-identification (symbol-name (eieio-oref transient--prefix (quote command)))) (if transient-enable-popup-navigation (set (make-local-variable (quote cursor-in-non-selected-windows)) (quote box)) (setq cursor-type nil)) (setq display-line-numbers nil) (setq show-trailing-whitespace nil) (transient--insert-groups) (if (or transient--helpp transient--editp) (progn (transient--insert-help))) (if (eq transient-mode-line-format (quote line)) (progn (insert (propertize "__" (quote face) (quote transient-separator) (quote display) (quote (space :height ...)))) (insert (propertize "\n" (quote face) (quote transient-separator) (quote line-height) t)))) (let ((window-resize-pixelwise t) (window-size-fixed nil)) (fit-window-to-buffer nil nil 1)) (goto-char (point-min)) (if transient-force-fixed-pitch (progn (transient--force-fixed-pitch))) (if transient-enable-popup-navigation (progn (transient--goto-button focus)))) (internal--after-with-selected-window save-selected-window--state))
(save-current-buffer (unwind-protect (progn (select-window (car save-selected-window--state) (quote norecord)) (if transient-enable-popup-navigation (progn (setq focus (button-get (point) (quote command))))) (erase-buffer) (set-window-hscroll transient--window 0) (set-window-dedicated-p transient--window t) (set-window-parameter transient--window (quote no-other-window) t) (setq window-size-fixed t) (if (and (boundp (quote tab-line-format)) tab-line-format) (progn (setq tab-line-format nil))) (setq mode-line-format (if (eq transient-mode-line-format (quote line)) nil transient-mode-line-format)) (setq mode-line-buffer-identification (symbol-name (eieio-oref transient--prefix (quote command)))) (if transient-enable-popup-navigation (set (make-local-variable (quote cursor-in-non-selected-windows)) (quote box)) (setq cursor-type nil)) (setq display-line-numbers nil) (setq show-trailing-whitespace nil) (transient--insert-groups) (if (or transient--helpp transient--editp) (progn (transient--insert-help))) (if (eq transient-mode-line-format (quote line)) (progn (insert (propertize "__" (quote face) (quote transient-separator) (quote display) (quote ...))) (insert (propertize "\n" (quote face) (quote transient-separator) (quote line-height) t)))) (let ((window-resize-pixelwise t) (window-size-fixed nil)) (fit-window-to-buffer nil nil 1)) (goto-char (point-min)) (if transient-force-fixed-pitch (progn (transient--force-fixed-pitch))) (if transient-enable-popup-navigation (progn (transient--goto-button focus)))) (internal--after-with-selected-window save-selected-window--state)))
(let ((save-selected-window--state (internal--before-with-selected-window transient--window))) (save-current-buffer (unwind-protect (progn (select-window (car save-selected-window--state) (quote norecord)) (if transient-enable-popup-navigation (progn (setq focus (button-get ... ...)))) (erase-buffer) (set-window-hscroll transient--window 0) (set-window-dedicated-p transient--window t) (set-window-parameter transient--window (quote no-other-window) t) (setq window-size-fixed t) (if (and (boundp (quote tab-line-format)) tab-line-format) (progn (setq tab-line-format nil))) (setq mode-line-format (if (eq transient-mode-line-format (quote line)) nil transient-mode-line-format)) (setq mode-line-buffer-identification (symbol-name (eieio-oref transient--prefix (quote command)))) (if transient-enable-popup-navigation (set (make-local-variable (quote cursor-in-non-selected-windows)) (quote box)) (setq cursor-type nil)) (setq display-line-numbers nil) (setq show-trailing-whitespace nil) (transient--insert-groups) (if (or transient--helpp transient--editp) (progn (transient--insert-help))) (if (eq transient-mode-line-format (quote line)) (progn (insert (propertize "__" ... ... ... ...)) (insert (propertize "\n" ... ... ... t)))) (let ((window-resize-pixelwise t) (window-size-fixed nil)) (fit-window-to-buffer nil nil 1)) (goto-char (point-min)) (if transient-force-fixed-pitch (progn (transient--force-fixed-pitch))) (if transient-enable-popup-navigation (progn (transient--goto-button focus)))) (internal--after-with-selected-window save-selected-window--state))))
(let ((buf (get-buffer-create transient--buffer-name)) (focus nil)) (if (window-live-p transient--window) nil (setq transient--window (display-buffer buf transient-display-buffer-action))) (let ((save-selected-window--state (internal--before-with-selected-window transient--window))) (save-current-buffer (unwind-protect (progn (select-window (car save-selected-window--state) (quote norecord)) (if transient-enable-popup-navigation (progn (setq focus ...))) (erase-buffer) (set-window-hscroll transient--window 0) (set-window-dedicated-p transient--window t) (set-window-parameter transient--window (quote no-other-window) t) (setq window-size-fixed t) (if (and (boundp ...) tab-line-format) (progn (setq tab-line-format nil))) (setq mode-line-format (if (eq transient-mode-line-format ...) nil transient-mode-line-format)) (setq mode-line-buffer-identification (symbol-name (eieio-oref transient--prefix ...))) (if transient-enable-popup-navigation (set (make-local-variable ...) (quote box)) (setq cursor-type nil)) (setq display-line-numbers nil) (setq show-trailing-whitespace nil) (transient--insert-groups) (if (or transient--helpp transient--editp) (progn (transient--insert-help))) (if (eq transient-mode-line-format (quote line)) (progn (insert ...) (insert ...))) (let ((window-resize-pixelwise t) (window-size-fixed nil)) (fit-window-to-buffer nil nil 1)) (goto-char (point-min)) (if transient-force-fixed-pitch (progn (transient--force-fixed-pitch))) (if transient-enable-popup-navigation (progn (transient--goto-button focus)))) (internal--after-with-selected-window save-selected-window--state)))))
transient--show()
#f(compiled-function (cl--cnm obj) "Highlight the infix in the popup buffer.\n\nAlso arrange for the transient to be exited in case of an error\nbecause otherwise Emacs would get stuck in an inconsistent state,\nwhich might make it necessary to kill it from the outside." #<bytecode 0x7447a2d>)(#f(compiled-function (&rest cnm-args) #<bytecode 0x5d6eb91>) #<org-ql-view--variable org-ql-view--variable>)
apply(#f(compiled-function (cl--cnm obj) "Highlight the infix in the popup buffer.\n\nAlso arrange for the transient to be exited in case of an error\nbecause otherwise Emacs would get stuck in an inconsistent state,\nwhich might make it necessary to kill it from the outside." #<bytecode 0x7447a2d>) #f(compiled-function (&rest cnm-args) #<bytecode 0x5d6eb91>) #<org-ql-view--variable org-ql-view--variable>)
#f(compiled-function (&rest args) #<bytecode 0x729f6b5>)(#<org-ql-view--variable org-ql-view--variable>)
apply(#f(compiled-function (&rest args) #<bytecode 0x729f6b5>) #<org-ql-view--variable org-ql-view--variable> nil)
transient-infix-read(#<org-ql-view--variable org-ql-view--variable>)
(transient-infix-set obj (transient-infix-read obj))
(let ((obj (transient-suffix-object))) (transient-infix-set obj (transient-infix-read obj)))
org-ql-view:--query()
funcall-interactively(org-ql-view:--query)
call-interactively(org-ql-view:--query nil nil)
command-execute(org-ql-view:--query)
The backtrace shows that the error happens in transient--show
, so looking at its source, I see where it does this:
(setq mode-line-buffer-identification
(symbol-name (oref transient--prefix command)))
So transient--prefix
is nil, which causes the error. But I have no idea why transient--prefix
is nil. That doesn't happen in Magit's code, e.g. from magit-log.el
:
(define-infix-argument magit:--author ()
:description "Limit to author"
:class 'transient-option
:key "-A"
:argument "--author="
:reader 'magit-transient-read-person)
Well, maybe define-infix-command
is the solution. Here's an example from magit-branch.el
:
(define-infix-command magit-branch.<branch>.rebase ()
:class 'magit--git-variable:choices
:scope 'magit--read-branch-scope
:variable "branch.%s.rebase"
:fallback "pull.rebase"
:choices '("true" "false")
:default "false")
And that uses these classes:
(defclass magit--git-variable (transient-variable)
((scope :initarg :scope)))
(defclass magit--git-variable:choices (magit--git-variable)
((choices :initarg :choices)
(fallback :initarg :fallback :initform nil)
(default :initarg :default :initform nil)))
(defclass transient-variable (transient-infix)
((variable :initarg :variable)
(format :initform " %k %d %v"))
"Abstract superclass for infix commands that set a variable."
:abstract t)
(defclass transient-infix (transient-suffix)
((transient :initform t)
(argument :initarg :argument)
(shortarg :initarg :shortarg)
(value :initform nil)
(multi-value :initarg :multi-value :initform nil)
(allow-empty :initarg :allow-empty :initform nil)
(history-key :initarg :history-key :initform nil)
(reader :initarg :reader :initform nil)
(prompt :initarg :prompt :initform nil)
(choices :initarg :choices :initform nil)
(format :initform " %k %d (%v)"))
"Transient infix command."
:abstract t)
(defclass transient-suffix (transient-child)
((key :initarg :key)
(command :initarg :command)
(transient :initarg :transient)
(format :initarg :format :initform " %k %d")
(description :initarg :description :initform nil))
"Superclass for suffix command.")
So I changed my implementation to use define-infix-command
instead of define-infix-argument
:
(define-infix-command org-ql-view:--query ()
;; :description "Edit query"
:class 'org-ql-view--variable
:key "-q"
:argument "--query="
:variable 'org-ql-view-query
:prompt "Query: "
:reader (lambda (prompt _initial-input history)
;; FIXME: Figure out how to integrate initial-input.
(read-string prompt (when org-ql-view-query
(format "%S" org-ql-view-query))
history)))
This causes the same backtrace.
I don't know where to go from here, other than diving down into the rabbit hole to find where transient--prefix
is set and try to figure out why it's nil when I run this code. But I think, as a user of the library, I'm not supposed to have to do that.
Any help would be appreciated.
Since I had no other explanation, I tried to construct a minimal working example based on Magit's usage:
(require 'transient)
(defclass argh--variable (transient-variable)
((scope :initarg :scope)))
(define-infix-command argh-set-query ()
"Set the `query' variable in the source buffer."
:class 'argh--variable
:key "-q"
:argument "--query="
:variable 'query)
(define-transient-command argh-transient ()
"Show transient for current buffer."
["Query"
(argh-set-query)])
(cl-defmethod transient-infix-set ((obj argh--variable) value)
"Set a variable."
(let ((variable (oref obj variable)))
(oset obj value value)
(set (make-local-variable (oref obj variable)) value)
(unless (or value transient--prefix)
(message "Unset %s" variable))))
That worked without any errors, and it set the variable in the buffer. So I then modified my org-ql-view
example to be exactly the same except for the appropriate symbol names, but the error persisted.
So I restarted Emacs and then my code worked. sigh Who knows where that state is that got messed up. Note that I was using emacs-lisp-byte-compile-and-load
on the file containing all of my code, so everything should have been re-evaluated properly. I also tried using C-M-x
on individual forms. So who knows!
So, bottom line: if nothing else works, and you can't figure out what the problem is, try restarting Emacs.
I just found this excellent presentation by Adrien Brochard showing step-by-step how to build a UI using tabulated-list-mode
and Transient: https://www.youtube.com/watch?v=w3krYEeqnyk I recommend listing it in the Transient manual. His notes: https://gist.github.com/abrochard/dd610fc4673593b7cbce7a0176d897de
My experiment here: gist
I have had limited success in creating two classes for boolean and string type variables in buffers. The first class is for boolean variables and inherits from transient-infix
. The second class inherits from transient-argument
. The key part is to override the transient methods: init-value
, infix-read
and infix-set
. In particular the init-value
methods must handle its entry in transient history.
For boolean variables, it is usable. But for string variables, there are two usability issues:
-
The transient UI does not show the current value. The infix is shown as if it was a switch instead of an argument. I could be missing something here because the infix created using the following form for comparision works as expected:
("a" "transient argument" "--argument=")
. -
The transient UI does not allow to change the infix value from a non-nil one to another non-nil one directly. On the first invocation of the infix command, the UI resets the infix value without prompt. The prompt shows up on the second invocation. Though this might be by design of
transient-argument
.
I've posted on https://emacs.stackexchange.com/questions/62382/how-to-deal-with-user-arguments-with-magit-transients but I thought it's also relevant for this as well. I'd be up to doing some tutorials as I'd like to migrate some hydras over the transient as it does seem to give more finer grain control over how it's used.

There are also some other questions i have about how to set headings, how to update the text based on state and, how to draw a border but it would be great if some really basic examples dealing with how to get data into and out of a transient are given.
The actual question:
I understand the infix flags work. However, I'm not sure how best to work with argument inputs. I have read the docs as well as looked at some examples from the magit source code but it's still a little bit beyond me as I need some really basic examples to get me started:
Given these two functions:
(defun say-hi:fn (&optional args)
(message "%s" (<read-args> args)))
(define-transient-command say-hi ()
"Say Hi"
["Arguments"
("-g" "Greeting" "--greeting")
("-n" "Name" "--name")]
["Actions"
("H" "Hi" say-hi:fn)])
-
How do I customise the default Greeting to be
"Hello"
and the default Name to be"World"
? -
How do I define
to sample <greeting>
and<name>
fromargs
? -
How do I limit the selection of
to be one of Hello
,Hi
andG'day
? -
How do I hook up
<name>
to be read from another input source such ascounsel
or a form widget?
I'd like to have a nice and simple search/replace
split pane viewer like that found in most editors instead of the current workflow. I started to do one with hydra
but the lack of intermediate state transitions means either to implement one yourself (which is hard because of the macros) or to hack it in by forcing the hydra to close and reopen on particular transitions.
Also, if transients can handle the history as well, then the design would be quite clean and simple to maintain.
Maybe some other user who has moved beyond this hurdle would like to take this one, please?
@zcaudate Don't ask the same question in three (and counting) places. I consider this to be rather impolite and it severely reduces my willingness to help you. Since I am a bit stressed out anyway, that probably means you will have to wait until next year for me to take a look at this.
@tarsius: Apologies. I assumed that you are busy by your first comment and wanted to broaden the range of people that might see it and respond. It wasn't meant to be 3 question all directed at you.
Though given the lack of responses... I'll just wait. It's fine.
FYI... Here is some addition context.
I have an emacs setup with a key-binding customiser. I've recently extracted it out. It used to be called etude-lang
and existed as part of my setup for customisation of key-bindings.
I wrote it quite a while back when moving from ido
to counsel
and having a ton of features break on me. It's a bit of a hack but I've since cleaned it up. Essentially eta
lets the user define their own actions
and then connects them to the key bindings and implementions. There is an additional component to the library that I have not yet extracted out - a macro to bind actions to a user defined menu
This is the part that is dependent on hydra
.
I want to experiment with transients
as the library and my current implementation using pretty-hydra
has almost the same syntax. The added bonus of transients
are that infixes
allow for better control of state transitions.
Anyways. The current setup is not perfect but I'm relatively happy with it. However, I know that it could definitely be a lot better when transients
are added.. Thus the eagerness.
I just found screenshot.el which uses transient. It's quite easy to follow and shows how to set elisp variables from options, see screenshot--define-infix. Maybe it can answer some of the above questions.
I'm trying to put together a simple example:
(defun bjc/beginning-of-buffer () "bob" (goto-char (point-min)))
(defun bjc/end-of-buffer () "eob" (goto-char (point-max)))
(transient-define-prefix bjc/test-transient-prefix
"Test some weird transient behavior."
["Buffer movement"
("B" "beginning of buffer" bjc/beginning-of-buffer)
("E" "end of buffer" bjc/end-of-buffer)]
["Character movement"
("f" "forward char" forward-char)
("b" "backward char" backward-char)])
(local-set-key (kbd "C-c t") 'bjc/test-transient-prefix)
When I hit C-c t
I get the error:
transient-setup: Suffix bjc/beginning-of-buffer is not defined or autoloaded as a command
I'm not sure why that is. Looking at where the error is emitted sheds no light for me (it appears to be checking what type of cl-obj
it is?) The documentation makes it look like I'm doing it right as well, from my reading.
One final wrinkle: if I change the calls to bjc/beginning-of-buffer
and bjc/end-of-buffer
to forward-char
and backward-char
, suddenly the transient works. And yes, I have triple-checked that the bjc/*
functions are defined and execute as expected.
A function becomes a command if it begins with an interactive
form. See Defining Commands in the elisp manual.
Thank you! Even after all these years it turns out I'm still missing basic Emacs nomenclature.
Recommend developing an example that uses a transient to explore the transient API, introspecting state and demonstrating behavior in a transient. I broke ground on such an example while learning the API.
Let's drive this issue towards closure
- finish this transient-in-transient demo
- implement more available behaviors
- demonstrate custom classes
- place it into the manual as a quickstart section
- possibly integrate such transient state & behavior introspection into the help workflows
With no objection, I'll drive towards a PR whenever I find the manual source and we can refine the demo there
Great idea! I took a look at your reddit post. Especially given that transient
may be integrated into Emacs core soon, this is critically needed. Some suggestions:
- Much of what you wrote is a well-crafted argument laying out why transient is a superior interface to a combinatorially large command space. There are many such command spaces in Emacs (and more every year). But my guess is that the typical reader of a "transient Quickstart" guide will already be convinced of this need! Much more important will be showing them how to implement their first transient.
- I took a look at the code provided. I suggest documenting each example one by one in a separate doc section, with a short and sweet "what this is good for", ideally with screenshots, building from simple to more complex.
- A good quick start guide would use easily evaluate-able code examples to take the reader through topics like:
- A transient "Hello World": Example with code of the simplest possible transient and how to bind it to a key like
C-c g
to activate it. - Adding simple boolean options: Adding a couple of boolean option flags like
--verbose
. - Taking actions from the transient: Something simple like
p (Print)
f (Fancy Print)
q (Quit)
- Setting options which take arguments: Adding a couple more options flag with arguments like
--name="frank"
,--age=35
. -
Limiting option values to a predetermined list: User can only choose from a small preset list of
--job
options. -
Mutually exclusive options: Alternatively, a set of options that are cycled among using a single key, e.g.
Job: --mailman|--plumber|--teacher
. -
Dynamically determined options: E.g.
--age
values drawn exclusively from a list of 3 random numbers. -
Option checking: Insisting
--age > 21
or--name
must be a number-free string. -
Setting variables: Recording persistent option values by setting variables outside of the transient, like
l language English
. - Organizing your transient into sections: Using the same transient built above, organize into option and action categories with nice labels.
- Hiding infrequently used options: Toggle display of more rarely used or "hidden" options common to all related transients.
- Transient commands that invoke sub-transients: Introduce a new action that brings up a sub-transient with its own simple option set.
- A transient "Hello World": Example with code of the simplest possible transient and how to bind it to a key like
Maybe there's more I'm missing? This is probably enough for most people. For the record, I only know how to do about half of these!
BTW, I do think the prefix/infix/suffix nomenclature is an impediment to the user hoping for a quick start. Instead of trying to branch out from Emacs' C-u
prefix notation, perhaps a simple intro can build on the user's familiarity with the other type of combinatorial option system they likely encounter every day: command line options. I suggest, at least for the simplified documentation, settling on something simple like command/option/action. Analogy:
Transient Nomenclature | Command-line Analogy | Command-line Example | Invoking in Emacs | Hypothetical Emacs example [*] = inside a transient |
---|---|---|---|---|
prefix | command | git |
Normal emacs command key binding | C-c g |
infix | option | git --version or git --git-dir=/my/path |
dash and key | [*]-v |
infix variable | environment variable | GIT_EDITOR=emacs |
variable key | [*]e (enter value in mini-buffer, Return) |
suffix | action | git --version [press Return] |
action key | [*]p |
other suffix | action | no direct analogy | other action key | [*]f |
sub-transient | sub-command | git show |
sub-transient key | [*]s |
sub-transient infix | sub-command option | git show --oneline |
dash and key in a sub-transient | [*]-o |
The one wrinkle to the command-line analogy is that transients are in fact more flexible, because they can have more than one "concluding action" (imagine having 10 different return keys on your keyboard that would execute the same command line in different ways!). Plus it's more straightforward for sub-actions to have sub-actions (which you could do on the command line but ...ughh...).
Happy to look over/try out anything you come up with!
I broke ground on such an example
@psionic-k thanks a lot for this effort. That looks very nice!
With no objection, I'll drive towards a PR
We should probably not add this to the manual because it serves a different purpose.
I haven't looked at it in to much detail yet, but The documentation system sounds interesting, especially the "four types of documentation" distinction.
Currently our manual has a strong focus on "reference", with a dash of "explanation". Some other manuals that I have written more strongly incorporate all four aspects and we could move in that direction, but I don't think it is not only a historic accident that this manual is more technical than the others I have written.
While there should of course be prominent cross-references (and even deep linking), I think that particularly in this case it is a good idea to keep the different documents separate. So I would suggest that you add this to the existing wiki. (Possibly but not necessarily immediately, do as you see fit.)
A good quick start guide would use easily evaluate-able code examples to take the reader through topics like
@jdtsmith that's the plan, more or less. ;-)
Maybe we should think a bit about the distinction between "tutorials" and "how-to guides" as two of the four documentation types. I am not clear on which of the two this would be.
You make some other good suggestions too.
I do realize that I will have to write some of this myself and curate whatever you and others come up, but please be aware that I fully support and appreciate if users write some documentation.
Don't worry about mistakes, just make me aware of what you have come up, I would be happy to point out any misunderstandings, which I would take as an opportunity to learn what aspects of the api are unclear. If you feel like using different terminology would help, then please do. Experiment with whatever aspect you want.
If you host your texts elsewhere, then please link to that from the wiki. You can also add to the wiki directly.
I'm happy to help write up a tutorial/how-to, but I find myself in the problematic position of needing to read at least a draft of such a tutorial first! For example, I think I can implement only ~half of the 12 simple exercises I mentioned above. And I have the distinct concern that I would be implementing them in a hackish or inflexible manner, since it has taken me a fair amount of playing around to get transient to do things.
To get the ball rolling, here's my effort of examples 1 and 2 (this was more complicated than it looks, since the vector slots of define-prefix
are overloaded to do so much work that they seem to be rather finicky):
(require 'transient)
;; Example 1: Hello World
(defun transient-hello-world-print ()
(interactive)
(message "Hello World!"))
(transient-define-prefix transient-hello-world ()
"A simple hello-world transient example."
[(transient-hello-world-print :key "p" :description "print Hello World" )])
(bind-key "C-c g" #'transient-hello-world)
;; Example 2: Hello World, adding a boolean option:
(defun transient-hello-world-print-options (args)
(interactive (list (transient-args 'transient-hello-world-options)))
(if (member "-v" args)
(message "A fine Hello to you this day, World!")
(message "Hello World!")))
(transient-define-prefix transient-hello-world-options ()
"A simple hello-world transient example with an option."
[ (:shortarg "-v" :description "Verbosity")
(transient-hello-world-print-options :key "p" :description "print Hello World" )])
(bind-key "C-c g" #'transient-hello-world-options)
I notice @psionic-k and I have quite distinct "styles"; perhaps @tarsius could comment on which (if either) is more idiomatic transient. It might then be good for us to settle on a simple list of exercises to show the potential user, work together to come up with the simplest/most idiomatic code "solutions", and then build up the text of the howto from there. I should have mentioned groups/columns/rows among my list, for example.
I'm happy to help write up a tutorial/how-to, but I find myself in the problematic position of needing to read at least a draft of such a tutorial first!
That could still be very useful. For example if someone else took care of the "story telling", then that would allow me to focus on the technical aspect without also trying to come up with useful or at least meaningful examples. Someone else could replace my dummy examples with something potentially useful. That would have the added benefit that the people who come up with better examples get inspired to implement even more useful transients intended for end users.
I notice @psionic-k and I have quite distinct "styles"; perhaps @tarsius could comment on which (if either) is more idiomatic transient.
@psionic-k's.
I notice @psionic-k and I have quite distinct "styles"; perhaps @tarsius could comment on which (if either) is more idiomatic transient. @psionic-k's.
Was afraid of that 😟 . I tend to prefer a more terse style which doesn't litter the name space with new functions for every simple option like -v
above. But if that's the preferred approach I can adapt. I can certainly help with the story-telling once we converge on an "examples to highlight" list, if you are willing to show us how you'd implement them. @psionic-k what are your thoughts?
I focused on a different aspect of the difference. Whether it is better to use a stand-alone infix definition or do it inline, depends on a few things, mainly complexity and whether the infix is shared between different prefixes. So both ways are "idiomatic". However if you do use the terse variant, then you should... well, use the terse variant of the terse variant:
-(transient-hello-world-print :key "p" :description "print Hello World")
+("p" "print Hello World" transient-hello-world-print)
especially the "four types of documentation" distinction
Let's not be too abstract here. Where are we going to put the code examples? It would be incorrect to merely store examples in one of my Positron repo's wikis. We don't want to add discoverability problems to the documentation structure.
@psionic-k what are your thoughts?
Frankly I'm happy if my examples work at this point. I think any examples will create context, and then better examples will be attracted to that context. We need a substrate for growth.
Still yet, whenever an API has multiple ways to express the same thing, examples of the equivalence are extremely helpful to illustrate the relationships between forms. It's likely unavoidable to have only one way to express things when a short-hand is introduced. We need both wherever it's easy to do the same thing more than one way.
I took the liberty of writing up a little intro on What is the Purpose of Transient on the wiki. Feel free to use any way.
Here's a good exercise for someone who wants to dig into (and document) transient: ibuffer is literally crying out for a simple nested transient!
Operations on marked buffers:
‘S’ - Save the marked buffers.
‘A’ - View the marked buffers in the selected frame.
‘H’ - View the marked buffers in another frame.
‘V’ - Revert the marked buffers.
‘T’ - Toggle read-only state of marked buffers.
‘L’ - Toggle lock state of marked buffers.
‘D’ - Kill the marked buffers.
‘M-s a C-s’ - Do incremental search in the marked buffers.
‘M-s a C-M-s’ - Isearch for regexp in the marked buffers.
‘r’ - Replace by regexp in each of the marked
buffers.
‘Q’ - Query replace in each of the marked buffers.
‘I’ - As above, with a regular expression.
‘P’ - Print the marked buffers.
‘O’ - List lines in all marked buffers which match
a given regexp (like the function ‘occur’).
‘X’ - Pipe the contents of the marked
buffers to a shell command.
‘N’ - Replace the contents of the marked
buffers with the output of a shell command.
‘!’ - Run a shell command with the
buffer’s file as an argument.
‘E’ - Evaluate a form in each of the marked buffers. This
is a very flexible command. For example, if you want to make all
of the marked buffers read-only, try using (read-only-mode 1) as
the input form.
‘W’ - As above, but view each buffer while the form
is evaluated.
‘k’ - Remove the marked lines from the *Ibuffer* buffer,
but don’t kill the associated buffer.
‘x’ - Kill all buffers marked for deletion.
Marking commands:
‘m’ - Mark the buffer at point.
‘t’ - Unmark all currently marked buffers, and mark
all unmarked buffers.
‘* c’ - Change the mark used on marked buffers.
‘u’ - Unmark the buffer at point.
‘DEL’ - Unmark the previous buffer.
‘M-DEL’ - Unmark buffers marked with MARK.
‘U’ - Unmark all marked buffers.
‘* M’ - Mark buffers by major mode.
‘* u’ - Mark all "unsaved" buffers.
This means that the buffer is modified, and has an associated file.
‘* m’ - Mark all modified buffers,
regardless of whether they have an associated file.
‘* s’ - Mark all buffers whose name begins and
ends with ‘*’.
‘* e’ - Mark all buffers which have
an associated file, but that file doesn’t currently exist.
‘* r’ - Mark all read-only buffers.
‘* /’ - Mark buffers in ‘dired-mode’.
‘* h’ - Mark buffers in ‘help-mode’, ‘apropos-mode’, etc.
‘.’ - Mark buffers older than ‘ibuffer-old-time’.
‘d’ - Mark the buffer at point for deletion.
‘% n’ - Mark buffers by their name, using a regexp.
‘% m’ - Mark buffers by their major mode, using a regexp.
‘% f’ - Mark buffers by their filename, using a regexp.
‘% g’ - Mark buffers by their content, using a regexp.
‘% L’ - Mark all locked buffers.
Filtering commands:
‘/ SPC’ - Select and apply filter chosen by completion.
‘/ RET’ - Add a filter by any major mode.
‘/ m’ - Add a filter by a major mode now in use.
‘/ M’ - Add a filter by derived mode.
‘/ n’ - Add a filter by buffer name.
‘/ c’ - Add a filter by buffer content.
‘/ b’ - Add a filter by basename.
‘/ F’ - Add a filter by directory name.
‘/ f’ - Add a filter by filename.
‘/ .’ - Add a filter by file extension.
‘/ i’ - Add a filter by modified buffers.
‘/ e’ - Add a filter by an arbitrary Lisp predicate.
‘/ >’ - Add a filter by buffer size.
‘/ <’ - Add a filter by buffer size.
‘/ *’ - Add a filter by special buffers.
‘/ v’ - Add a filter by buffers visiting files.
‘/ s’ - Save the current filters with a name.
‘/ r’ - Switch to previously saved filters.
‘/ a’ - Add saved filters to current filters.
‘/ &’ - Replace the top two filters with their logical AND.
‘/ |’ - Replace the top two filters with their logical OR.
‘/ p’ - Remove the top filter.
‘/ !’ - Invert the logical sense of the top filter.
‘/ d’ - Break down the topmost filter.
‘/ /’ - Remove all filtering currently in effect.
Filter group commands:
‘/ g’ - Create filter group from filters.
‘/ P’ - Remove top filter group.
‘TAB’ - Move to the next filter group.
‘M-p’ - Move to the previous filter group.
‘/ \’ - Remove all active filter groups.
‘/ S’ - Save the current groups with a name.
‘/ R’ - Restore previously saved groups.
‘/ X’ - Delete previously saved groups.
Sorting commands:
‘,’ - Rotate between the various sorting modes.
‘s i’ - Reverse the current sorting order.
‘s a’ - Sort the buffers lexicographically.
‘s f’ - Sort the buffers by the file name.
‘s v’ - Sort the buffers by last viewing time.
‘s s’ - Sort the buffers by size.
‘s m’ - Sort the buffers by major mode.
Other commands:
‘g’ - Regenerate the list of all buffers.
Prefix arg means to toggle whether buffers that match
‘ibuffer-maybe-show-predicates’ should be displayed.
‘`’ - Change the current display format.
‘SPC’ - Move point to the next line.
‘C-p’ - Move point to the previous line.
‘h’ - This help.
‘=’ - View the differences between this buffer
and its associated file.
‘RET’ - View the buffer on this line.
‘o’ - As above, but in another window.
‘C-o’ - As both above, but don’t select
the new window.
‘b’ - Bury (not kill!) the buffer on this line.
I started adding some API examples to https://github.com/magit/transient/wiki/Developer-Quick-Start-Guide
To start, I covered some of the tangled sources of confusion:
- Use of eieio looks like a foreign lisp feature if you are new to elisp. This is a barrier to reading magit source at first
- Terminology needs a crash course. It shouldn't tell lies, but it should get the main points across faster
- There are three ways to set most slots
Anyway, I feel good about this starting point. These are the behaviors I can think of to cover now. Please chip in if I'm missing some:
- ~infix switch, arguments~
- ~default switch & argument values, compact style~
- ~mutually exclusive switches~
- ~mode predicates, predicates~
- ~levels~
- ~infix with choices (has anyone gotten this working? can't recall if I have one)~
- interactive forms with mini-buffer prompts (because this supplements transient UX)
- ~reading arguments & prefixes into commands~
- call a command line program using arguments
- argument state persistence
- using objects as values (custom variable types)
- sharing state in nested transients
At this point, user can make lots of menus. The menus can be easy to learn. Are they efficient? They are probably more efficient than a command line tool with completions. However, the optimum is to create a UI that acts like a DSL with intelligent argument inference & prediction. At this optimum, the user frequently presses verb
verb
verb
and everything just works. Properly identifying the domain's objects & verbs are critical. The transient-on-transient example I'm building now suffers from improper separation of objects & verbs. It's what made me aware of the problem.
Hopefully a good demonstration problem will come to me. Bad CLI's or bad special modes are a great example, but we frequently only cover a small portion in behavior and by the time you cover them all, you get magit
and it's "too big" again.