magit / transient

Transient commands
https://magit.vc/manual/transient
GNU General Public License v3.0
707 stars 65 forks source link

Add some examples of basic transients and generally improve the documentation #51

Closed sehnsucht13 closed 1 year ago

sehnsucht13 commented 5 years ago

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.

tarsius commented 5 years ago

I plan to write some tutorial soonish.

Of course it you feel like it, there's nothing to keep you from doing the same.

jdtsmith commented 4 years ago

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

Silex commented 4 years ago

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

jdtsmith commented 4 years ago

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.

alphapapa commented 4 years ago

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:

  1. M-x org-ql-view-edit RET. This shows the Transient buffer.
  2. -q. This causes a wrong-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`:
```el
(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.

alphapapa commented 4 years ago

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.

alphapapa commented 4 years ago

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

zhaojiangbin commented 4 years ago

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:

  1. 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=").

  2. 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.

zcaudate commented 3 years ago

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.

Screen Shot 2020-12-20 at 1 27 14 am

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.

zcaudate commented 3 years ago

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)])
  1. How do I customise the default Greeting to be "Hello" and the default Name to be "World"?

  2. How do I define to sample <greeting> and <name> from args?

  3. How do I limit the selection of to be one of Hello, Hi and G'day?

  4. How do I hook up <name> to be read from another input source such as counsel or a form widget?

zcaudate commented 3 years ago

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.

tarsius commented 3 years ago

Maybe some other user who has moved beyond this hurdle would like to take this one, please?

tarsius commented 3 years ago

@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.

zcaudate commented 3 years ago

@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.

zcaudate commented 3 years ago

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.

sheijk commented 3 years ago

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.

bjc commented 3 years ago

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.

tarsius commented 3 years ago

A function becomes a command if it begins with an interactive form. See Defining Commands in the elisp manual.

bjc commented 3 years ago

Thank you! Even after all these years it turns out I'm still missing basic Emacs nomenclature.

psionic-k commented 3 years ago

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

With no objection, I'll drive towards a PR whenever I find the manual source and we can refine the demo there

jdtsmith commented 3 years ago

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:

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!

tarsius commented 3 years ago

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.

jdtsmith commented 3 years ago

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.

tarsius commented 3 years ago

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.

jdtsmith commented 3 years ago

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?

tarsius commented 3 years ago

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)
psionic-k commented 3 years ago

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.

jdtsmith commented 3 years ago

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.

jdtsmith commented 3 years ago

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.
psionic-k commented 3 years ago

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:

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:

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.

tarsius commented 3 years ago

Looks good and useful so far. Thanks!

The "Objects & EIEIO" section not so much:

There is no new syntax being used. It's just vanilla plists.

In newer Emacsen objects are implemented using structs, which do have a "new syntax" when written by hand: #s(...), though one usually does not write that. Still it seems weird to make this not quite correct information even bold. I think it never was "just vanilla plists". Plain lists yes, but not plists.

You can use eieio API's to explore transient objects. Let's look at some transients you have already:

(get 'magit-log 'transient--layout)
(get 'magit-log 'transient--prefix)

The first does not return an object.

psionic-k commented 3 years ago

Thanks for the proof read. It's quite necessary as I'm discovering both eieio and how to introspect magit and transient at the same time. I had been wondering about the #s(... style output.

The main point I wanted to communicate is that :property style API is not any kind of non-list magic. I've added some revision to avoid telling lies.

Added examples for many styles of argument use. Wow, still so many examples to cover. :factory_worker:

Btw, link updated to https://github.com/magit/transient/wiki/Developer-Quick-Start-Guide

psionic-k commented 3 years ago

https://github.com/magit/transient/wiki/Developer-Quick-Start-Guide#choices-from-a-function

I could not find what the arguments are to the choices function by looking at source. Any help?

psionic-k commented 3 years ago

CLI for cowsay requires a couple behaviors I'm not quite sure how to approach.

There's a few options for eyes. One of them is custom eyes, such as -e@@. Handling it with switches made the map cluttered, so I was going to put all of the eye options in a sub-prefix.

psionic-k commented 3 years ago

Hello everyone, I'm switching gears to fixing up and expanding some behaviors in transient itself.

I just finished more updates to the examples at https://github.com/magit/transient/wiki/Developer-Quick-Start-Guide are now in org format an most src blocks will show a meaningful example when executed.

Because it's org form, I left TODO stubs in the edit source. (My local copy is my TODO to finish documenting). If it's raining where you're at, please consider developing an example for any TODO's.

At this point, I think after working out most of the examples, most people should be able to digest magit source and the manual well enough to get the rest of the way.

The reason I'm going to work on transient itself is because I need certain behaviors to realize what I had intended to place in some examples. I will add examples as I get new behaviors implemented.

If anyone finds this thread and has questions, please feel free to get a hold of me either via email or Reddit etc.

psionic-k commented 1 year ago

I published an org literate guide. It tangles into transient-showcase.el, a package. You can run ts-showcase and it has about 20-30 examples bound.

https://github.com/positron-solutions/transient-showcase

I may have gone a little bit too far. And yet there still things that are not demonstrated :grin:

The wiki we had worked on needs an overhaul as any examples I put into it originally are now in the showcase, and I found plenty of small mistakes along the way.

I want to work on some small improvements and also applications I intended to write before worrying about going deeper on examples.

People who wanted something easier to understand than magit source should help with these examples by running and understanding them :pray:

tarsius commented 1 year ago

Sorry for not responding earlier. I was quite busy when you posted this and then I somehow forgot.

Looks exciting! I'll have a look shortly. (But now it's bedtime :sleeping: .)

tarsius commented 1 year ago

I've now added a prominent link to this tutorial/showcase at the beginning of them manual.

tarsius commented 1 year ago

Closing in favor of #127.