reddit / emacs
Using `completing-read` to call org-agenda custom commands
graduale · 2022-10-28 15:10
I'm looking for a way to bypass `org-agenda` dispatcher (unsure about the terminology here: I'm talking about the buffer you get immediately upon calling `org-agenda`), and thought of a simple way of doing it.
(defun my/org-agenda ()
(interactive)
(setq my/agenda-views nil)
(dolist (cmd org-agenda-custom-commands)
(add-to-list 'my/agenda-views (cons (car (cdr cmd)) (car cmd))))
(let ((view (cdr (assoc (completing-read "Select an agenda view: " my/agenda-views) my/agenda-views))))
(org-agenda nil view nil)))
That worked just fine, but I thought it'd be nice to be able to pass restrictions (to buffer, subtree, etc.) to the function, perhaps using `C-u` as a prefix for a buffer restriction and `C-u C-u` for a subtree restriction. So I figured this should work:
(defun my/org-agenda-restr (&optional arg)
(interactive "P")
(when arg (org-check-for-org-mode))
(setq my/agenda-views nil)
(dolist (cmd org-agenda-custom-commands)
(add-to-list 'my/agenda-views (cons (car (cdr cmd)) (car cmd))))
(let ((restr (cond ((equal arg '(4)) 'buffer)
((equal arg '(16)) 'subtree)))
(view (cdr (assoc (completing-read "Select an agenda view: " my/agenda-views) my/agenda-views))))
(org-agenda nil view restr)))
But it doesn't. For some reason, no matter which custom view I try to call, I get a `CRM` prompt asking me to choose a keyword.
Now, I thought maybe I was passing the wrong type of object as the third argument for `org-agenda`, So I tested using this, and it looks like what I'm passing as the third argument is a symbol, which I thought is what it wanted:
(defun my/test (&optional arg)
(interactive "P")
(let ((restr (cond ((equal arg '(4)) 'buffer)
((equal arg '(16)) 'subtree))))
(when (symbolp restr) (message "Yes"))))
So I'm going wrong somewhere, but cannot figure out where.
**EDIT**: Thanks to u/github-alphapapa, I ended up streamlining the definition above and found a way to get what I wanted. What did the trick, though I do not understand why that is necessary, was to call `org-agenda` in the scope of a `let` expression binding `current-prefix-arg` to `nil`. So, if anyone is interested, here's what seems to work so far:
(defun my/org-agenda-rest (&optional arg)
"Prompt to select a custom agenda view and display the agenda,
bypassing the dispatch buffer."
(interactive "P")
(when arg (org-check-for-org-mode))
(let* ((views
(cl-loop for cmd in org-agenda-custom-commands
collect (cons (car (cdr cmd)) (car cmd))))
(view (alist-get (completing-read "Select and agenda view: " views) views nil nil #'equal))
(restr (pcase current-prefix-arg
('(4) 'buffer)
('(16) 'subtree)
(_ nil))))
(let ((current-prefix-arg))
(org-agenda nil view restr))))
I'll probably be asking another question here to see if anyone can help me understand how `current-prefix-arg` works. But that's a whole other issue.
github-alphapapa · 2022-10-30 14:20
1. Using a global variable like that is an anti-pattern. Instead, use a `let`-bound local variable.
2. Same for using `add-to-list`. Instead, use `push` or `loop` to build lists.
3. Similarly, using `alist-get` or `map-elt` is easier than `(cdr (assoc ...`.
4. You may find it easier to test the value of `current-prefix-arg` in the `interactive` form, then provide arguments to your function as needed.
5. As you may have noticed, the `org-agenda` API is not always easy to work with. This is one of the reasons I wrote `org-ql`. You can easily configure an Org QL view with a Transient dispatcher, then save it, bookmark it, or link to it from in an Org document, and then you can access the saved views easily in various ways, without writing any code.
graduale · 2022-10-30 18:44
Thank you! This is very helpful. Following your advice, I ended up with this next attempt:
(defun my/org-agenda-rest (&optional arg)
"Prompt to select a custom agenda view and display the agenda,
bypassing the dispatch buffer."
(interactive "P")
(when arg (org-check-for-org-mode))
(let* ((views
(cl-loop for cmd in org-agenda-custom-commands
collect (cons (car (cdr cmd)) (car cmd))))
(view (alist-get (completing-read "Select and agenda view: " views) views nil nil #'string-equal))
(cond ((equal current-prefix-arg '(4)) (org-agenda nil view 'buffer))
((equal current-prefix-arg '(16)) (org-agenda nil view 'subtree))
(t (org-agenda nil view nil)))))
Now the function sort of works: for some views, I get what I want. But for others, I get the `CRM` prompt to select a keyword. I'm baffled.
I've been meaning to look into `org-ql`, so I'll definitely move that up to the top of the list!
yantar92 · 2022-10-31 06:39
`view` argument must be non-nil to suppress the prompt.
graduale · 2022-10-31 12:27
Thanks for taking a look. I'm not sure that's the issue, though. I'm getting the exact same behavior with this:
(defun my/org-agenda-rest (&optional arg)
"Prompt to select a custom agenda view and display the agenda,
bypassing the dispatch buffer."
(interactive "P")
(when arg (org-check-for-org-mode))
(let* ((views
(cl-loop for cmd in org-agenda-custom-commands
collect (cons (car (cdr cmd)) (car cmd))))
(view (alist-get (completing-read "Select and agenda view: " views) views nil nil #'string-equal)))
(when view
(cond ((equal current-prefix-arg '(4)) (org-agenda nil view 'buffer))
((equal current-prefix-arg '(16)) (org-agenda nil view 'subtree))
(t (org-agenda nil view nil))))))
Unless I've misunderstood something, of course...
yantar92 · 2022-10-31 12:31
I do not see anything obvious. You may try M-x debug-on-entry <RET> org-agenda <RET> and the stepping through the code. Agenda dialogue is raised by `org-agenda-get-restriction-and-command`
github-alphapapa · 2022-10-31 16:37
You learn very quickly. Well done! A few notes:
1. In my testing, strangely enough, `equal` performs better than `string-equal`/`string=`, so I always just use `equal` (not that it matters in this scenario, but it's good to know).
2. If you haven't already, you should probably review all of the arguments to `completing-read`.
3. Since the only difference between the 3 outcomes is the final argument to `org-agenda`, you could just call that function at the end of your function, and bind the final argument to another variable; it would further simplify the code.
4. Not that it matters, but you could use `(pcase current-prefix-arg ...` rather than `cond`, for even cleaner code.
5. Finally, regarding the baffling behavior: this is when you should reach into your toolbox for Edebug and step through the code. It should help you quickly locate the problem. i.e. `C-u C-M-x` on the `defun` form, then call the command, and step through with `n`, `SPC`, `f`, `o`, etc. (The full Edebug documentation is thorough but a bit much to digest at once.)
graduale · 2022-10-31 20:05
Thanks! That's very kind of you to say. Following your advice, I ended up with this:
(defun my/org-agenda-rest (&optional arg)
"Prompt to select a custom agenda view and display the agenda,
bypassing the dispatch buffer."
(interactive "P")
(when arg (org-check-for-org-mode))
(let* ((views
(cl-loop for cmd in org-agenda-custom-commands
collect (cons (car (cdr cmd)) (car cmd))))
(view (alist-get (completing-read "Select and agenda view: " views) views nil nil #'equal))
(restr (pcase current-prefix-arg
('(4) 'buffer)
('(16) 'subtree)
(_ nil))))
(org-agenda nil view restr)))
After somewhat blindly stepping through the function, I think I see what's going on. Somehow, when `org-agenda` is called, `org-todo-list` is called interactively, which results in `(4)` being passed as argument to it, which in turn results in my being prompted to select a keyword. This is the relevant bit of the backtrace:
org-todo-list((4))
funcall-interactively(org-todo-list (4))
call-interactively(org-todo-list)
#f(compiled-function () #<bytecode -0x1adbe23b2c54dd99>)()
funcall(#f(compiled-function () #<bytecode -0x1adbe23b2c54dd99>))
(let ((org-agenda-overriding-header '"") (org-agenda-hide-tags-regexp '"someday\\|refile") (org-super-agenda-groups '((:name "In progress" :todo "ONIT") (:name "Refile" :and (:not (:todo ...) :tag ("refile"))) (:name "Next available actions" :and (:todo "NEXT" :scheduled future) :and (:todo "NEXT" :deadline future)) (:name "Coming up" :and (:todo "TODO" :scheduled (before "2022-11-07") :scheduled future)) (:name "Important and unscheduled" :and (:priority>= "B" :scheduled nil :not (:tag ...))) (:name "On hold" :todo ("WAIT" "HOLD")) (:name "To process" :todo "ROAM") (:name "Etc" :and (:scheduled nil :not (:tag ...) :not (:todo "IDEA")) :discard (:anything t))))) (funcall '#f(compiled-function () #<bytecode -0x1adbe23b2c54dd99>)))
eval((let ((org-agenda-overriding-header '"") (org-agenda-hide-tags-regexp '"someday\\|refile") (org-super-agenda-groups '((:name "In progress" :todo "ONIT") (:name "Refile" :and (:not ... :tag ...)) (:name "Next available actions" :and (:todo "NEXT" :scheduled future) :and (:todo "NEXT" :deadline future)) (:name "Coming up" :and (:todo "TODO" :scheduled ... :scheduled future)) (:name "Important and unscheduled" :and (:priority>= "B" :scheduled nil :not ...)) (:name "On hold" :todo ("WAIT" "HOLD")) (:name "To process" :todo "ROAM") (:name "Etc" :and (:scheduled nil :not ... :not ...) :discard (:anything t))))) (funcall '#f(compiled-function () #<bytecode -0x1adbe23b2c54dd99>))))
org-agenda-run-series("Planning view" (((agenda "" ((org-agenda-span 'day) (org-super-agenda-groups '(... ... ... ... ...)))) (alltodo "" ((org-agenda-overriding-header "") (org-agenda-hide-tags-regexp "someday\\|refile") (org-super-agenda-groups `(... ... ... ... ... ... ... ...)))))))
org-agenda(nil "p" buffer)
What I don't get is why this isn't happening when I call `my/org-agenda-rest` with `C-u C-u` as prefix, nor when I call this other function:
(defun my/org-agenda-buffer (&optional arg)
"Prompt to select a custom agenda view and display the agenda,
bypassing the dispatch buffer."
(interactive "P")
(when arg (org-check-for-org-mode))
(let* ((views
(cl-loop for cmd in org-agenda-custom-commands
collect (cons (car (cdr cmd)) (car cmd))))
(view (alist-get (completing-read "Select and agenda view: " views) views nil nil #'equal)))
(org-agenda nil view 'buffer)))
Interestingly, if I call `my/org-agenda-buffer` with the `C-u` prefix, I am again prompted to select a keyword. I'll keep investigating...
graduale · 2022-10-31 20:48
OK, I think this has nothing to do with `org-agenda`, but with the way prefix arguments are passed on to other functions. I'll probably need to get clearer on that.
Consider these three functions:
(defun my/test (&optional arg)
(interactive "P")
(if arg
current-prefix-arg
"No argument"))
(defun my/other-test (&optional arg)
(interactive "P")
(message "%s, %s" current-prefix-arg (my/test arg)))
(defun my/final-test (&optional arg)
(interactive "P")
(message "%s, %s" (my/test nil) (my/other-test nil)))
If I call `my/final-test` with `C-u` as prefix argument, `my/other-test` ends up being sensitive to that, so that rather than outputting `No argument, nil, No argument`, it outputs `No argument, (4), No argument`.
So the question now is: how can I make use of `current-prefix-arg` so that, say, `my/function` can make use of it but any function in the body of `my/function` ignores the prefix I used to call `my/function`? I'll need to read some more...
EDIT: actually, the real question is: how can I make us of prefix arguments in functions whose body calls on other functions that use `current-prefix-argument` in their definition? The problem I'm running into arises because `org-todo-list` makes use of `current-prefix-arg`, and `org-todo-list` is being called by `org-agenda`, which is in turn being called by the function I'm defining.
EDIT (2): possibly related: https://lists.gnu.org/archive/html/emacs-orgmode/2022-07/msg00155.html
github-alphapapa · 2022-11-04 18:29
Great job again. Since you seem to appreciate my nit picking, here are a few more:
1. In the `cl-loop`, you can destructure the values of `org-agenda-custom-commands`, like `(cl-loop for (foo . (bar . baz)) in org-agenda-custom-commands ...)`, so in the `collect` clause you could use nicer-looking forms than `(car (cdr cmd ...`.
2. Not that it matters in your personal code, but the docstring is indented unidiomatically. You can use the `checkdoc` command to fix this.
3. Some of the code isn't indented properly. I strongly recommend using `aggressive-indent-mode` for Elisp code, because it ensures that your code is always indented correctly without any effort on your part.
Regarding the behavior, I don't have time to debug it myself right now, but maybe it will help to draw your attention to the function `call-interactively`, and the distinction between a regular function call and an interactive one.
graduale · 2022-11-09 01:56
Thanks again! I'll implement your suggestions (nit picking always appreciated) and see what I come up with (TIL about destructuring: intriguing!).
That said, I think I sort of figured out the source of the problem: simply let- binding `current-prefix-argument` to `nil` before the call to `org-agenda` solved it. I say 'sort of' because I'm still not sure I understand the behavior of `current-prefix-argument`. (I asked about this [somewhere else](https://www.reddit.com/r/emacs/comments/yix8yg/shouldnt_a_function_called_in_the_body_of_another/), but I'm still somewhat confused.)