Hi again!

Please find attached a new version of the patch for review.

Den lör 19 apr. 2025 kl 16:30 skrev Ihor Radchenko <yanta...@posteo.net>:

> Probably which-key (it is built-in).
>
I'm still a bit stuck with this. If org-menu-system is org-menu-which-key,
it will display a which-key menu, but I haven't gotten the keybindings to
work yet, I am probably making some silly mistake? After playing with this
for a day, I still can't get it to work in situ (I have had multiple
working toy examples, not sure how to proceed)

> +(org-menu-define org-cite-basic-follow (citation-object)
> > +                 "Follow citations"
>
> You can use (declare (indent defun)) in macro definition to make the
> indentation work as in defun. See various macros in org-macs.el as
> examples.
>
Ok, thanks and added!


> > +                 '[["Open"
>
> Is quote necessary here?
>
No, it isn't, removed.

>
> > +                    ("b" "bibliography entry" (org-cite-basic-goto
> !citation-object !prefix))]
> > ...
> > +                 :default-action 'org-cite-basic-goto
>
> There is inconsistency here.
> You can specify the exact arguments passed to menu-actions, but not to
> default action. I think that :default-action should follow a shortened
> menu-action spec.
>
Thanks for spotting this, it is now fixed.

>
> > +                   ["Copy"
> > +                    ("d" "DOI"
> > +                     (let ((doi (org-cite-basic--get-field
> > +                                 'doi
> > +                                 (org-cite-basic--get-key
> !citation-object))))
> > +                       (if (not (s-blank? doi))
> > +                           (kill-new doi)
> > +                         (user-error "No DOI for `%s'"
> (org-cite-basic--get-key !citation-object)))))
>
> Anonymous action definition is good for testing, but we should factor it
> out to a separate function at the end

I have added getters for doi and url fields - should we also add functions
to copy and browse to those? I feel this is maybe a bit too much?

> .
>
> > +                   ["Browse"
> > +                    ("w" "Browse URL/DOI"
>
> Maybe it should be under "Open" section as well.
>
> > +                 :parameter-types ('citation 'citation-reference))
>
> I believe the :parameter-types is way too specific for menu system for
> be truly generic. Imagine if someone wants to have more than one
> argument in a menu: (org-menu-define test (arg1 arg2 &optional arg3)
> Rather than implicitly defining interactive spec as in
>
> > +         (interactive
> > +          (list (let ((obj (org-element-context)))
> > +                  (pcase (org-element-type obj)
> > +                    ((or ,@types) obj)
> > +                    (_ (user-error "Wrong object type"))))))
>
> you should let the caller handle interactive spec and other
> argument pre-processing.
>
> > +(cl-defmacro org-menu-define (name
> > +                              arglist
> > +                              docstring
> > +                              contents
> > +                              &key ((:default-action default-action)
> nil)
> > +                              &key ((:parameter-types types) nil))
>
> I suggest adding &rest body to the macro definition
> and then placing that body directly before
> > + (cond ((not (xor org-menu-mode
>
> WDYT?
>
I think this makes sense together with the argument list flexibility,
please find an attempt to implement this in the attached patch


>
> > +
> > +;;; Main macro definition
> > +(cl-defmacro org-menu-define (name
> > +                              arglist
> > +                              docstring
> > +                              contents
> > +                              &key ((:default-action default-action)
> nil)
> > +                              &key ((:parameter-types types) nil))
> > ...
> > +        (complete-arglist (if (member 'prefix arglist)
> > +                              arglist
> > +                              `(,@arglist prefix))))
> > +    `(progn
> > +
> > +;;; Local customization
>
> You are overdoing the page breaks and comments.
> Please do not split macro body with section comments and page breaks.
> Instead, I recommend defining a set of helper macros:
> 1. For each defcustom
> 2. For defun
> 3. for transient-define-suffic
>
> Then, you can easily separate code for and place comments without
> breaking a defmacro body.
>
I tried this, but felt it became harder to read, so I broke out some helper
functions but kept the defcustoms in the macro definition. Are you ok with
this?


>
> > +       (defcustom ,menu-actions ,contents
> > +         ,(concat "Actions in the `"
> > +                  (symbol-name name)
> > +                  "' org menu.
> > +
> > +This option uses the same syntax as `transient-define-prefix', see Info
> node
> > +`(transient)Binding Suffix and Infix Commands'. In addition, it is
> possible
> > +to specify a function call for the COMMAND part, where ARGUMENTS can be
> used
> > +to access those values.")
> > +         :group 'org-menu
> > +         :package-version '(Org . "9.8")
>
> :package-version makes no sense here.
>
> Also, it is a good idea to provide some examples in the docstring.
> For users and also to make sure that we understand the org-menu design
> ourselves :)
>
Nope, removed! I tried to flesh out the doc strings a bit.

Thanks again for all the help and guidance!
Cheers,
Tor-björn
From 6e4a300096b47f21880b949d17eed6929e63f2e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tor-bj=C3=B6ron?= <tclaes...@gmail.com>
Date: Thu, 27 Feb 2025 20:30:07 +0200
Subject: [PATCH] lisp/om.el: Org-menu, a simple menu system for org.

* lisp/om.el: Add org-menu.
* lisp/oc-basic.el (require 'om): Pull in om.
(org-cite-basic-follow-actions): New customization option, that
specifies the contents of the transient menu.
(org-cite-basic--get-key): New function. Get citation key from
citation or citation reference.
(org-cite-basic--get-url): New function. Get URL from citation or
citation reference.
(org-cite-basic--get-doi): New function. Get DOI from citation or
citation reference.
(org-cite-basic-goto): Use org-cite-basic--get-key.
(org-cite-basic-follow): Add a citation follower using org-menu.
(org-cite-register-processor 'basic): Update the basic citation
processor to follow citations using `org-cite-basic-follow'.

This change was co-authored with much support from Ihor Radchenko and
Jonas Bernoulli, thanks!
---
 lisp/oc-basic.el |  68 +++++++++--
 lisp/om.el       | 299 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 358 insertions(+), 9 deletions(-)
 create mode 100644 lisp/om.el

diff --git a/lisp/oc-basic.el b/lisp/oc-basic.el
index 32a99e987..46fad5c52 100644
--- a/lisp/oc-basic.el
+++ b/lisp/oc-basic.el
@@ -74,10 +74,12 @@
 (require 'map)
 (require 'oc)
 (require 'seq)
+(require 'om)
 
 (declare-function org-open-at-point "org" (&optional arg))
 (declare-function org-open-file "org" (path &optional in-emacs line search))
 
+(declare-function org-element-context "org-element" (&optional element))
 (declare-function org-element-create "org-element-ast" (type &optional props &rest children))
 (declare-function org-element-set "org-element-ast" (old new &optional keep-props))
 
@@ -326,6 +328,28 @@ INFO is the export state, as a property list."
                 (map-keys entries))
               (org-cite-basic--parse-bibliography)))
 
+(defun org-cite-basic--get-key (citation-or-citation-reference)
+  "Return citation key for CITATION-OR-CITATION-KEY."
+  (if (org-element-type-p citation-or-citation-reference 'citation-reference)
+      (org-element-property :key citation-or-citation-reference)
+    (pcase (org-cite-get-references citation-or-citation-reference t)
+      (`(,key) key)
+      (keys
+       (or (completing-read "Select citation key: " keys nil t)
+           (user-error "Aborted"))))))
+
+(defun org-cite-basic--get-url (citation-or-citation-reference)
+  "Return URL for CITATION-OR-CITATION-KEY."
+  (org-cite-basic--get-field
+   'url
+   (org-cite-basic--get-key citation-or-citation-reference)))
+
+(defun org-cite-basic--get-doi (citation-or-citation-reference)
+  "Return DOI for CITATION-OR-CITATION-KEY."
+  (org-cite-basic--get-field
+   'doi
+   (org-cite-basic--get-key citation-or-citation-reference)))
+
 (defun org-cite-basic--get-entry (key &optional info)
   "Return BibTeX entry for KEY, as an association list.
 When non-nil, INFO is the export state, as a property list."
@@ -805,14 +829,7 @@ export state, as a property list."
 When DATUM is a citation reference, open bibliography entry referencing
 the citation key.  Otherwise, select which key to follow among all keys
 present in the citation."
-  (let* ((key
-          (if (org-element-type-p datum 'citation-reference)
-              (org-element-property :key datum)
-            (pcase (org-cite-get-references datum t)
-              (`(,key) key)
-              (keys
-               (or (completing-read "Select citation key: " keys nil t)
-                   (user-error "Aborted"))))))
+  (let* ((key (org-cite-basic--get-key datum))
          (file
           (pcase (seq-find (pcase-lambda (`(,_ . ,entries))
                              (gethash key entries))
@@ -832,6 +849,39 @@ present in the citation."
        (bibtex-set-dialect)
        (bibtex-search-entry key)))))
 
+(org-menu-define org-cite-basic-follow (citation-object)
+                 "Follow citations"
+                 [["Open"
+                   ("b" "bibliography entry" (org-cite-basic-goto !citation-object !prefix-argument))
+                   ("w" "Browse URL/DOI"
+                    (let ((url (org-cite-basic--get-url !citation-object))
+                          (doi (org-cite-basic--get-doi !citation-object)))
+                      (cond ((not (s-blank? url))
+                             (browse-url url))
+                            ((not (s-blank? doi))
+                             (if (string-match "^http" doi)
+                                 (browse-url doi)
+                               (browse-url (format "http://dx.doi.org/%s"; doi))))
+                            (t (user-error "No URL or DOI for `%s'"
+                                           (org-cite-basic--get-key !citation-object))))))]             
+                  ["Copy"
+                   ("d" "DOI"
+                    (let ((doi (org-cite-basic--get-doi !citation-object)))
+                      (if (not (s-blank? doi))
+                          (kill-new doi)
+                        (user-error "No DOI for `%s'" (org-cite-basic--get-key !citation-object)))))
+                   ("u" "URL"
+                    (let ((url (org-cite-basic--get-url !citation-object)))
+                      (if (not (s-blank? url))
+                          (kill-new url)
+                        (user-error "No URL for `%s'" (org-cite-basic--get-key !citation-object)))))]]
+                 (org-cite-basic-goto !citation-object !prefix-argument)
+                 (interactive
+                  (list (let ((obj (org-element-context)))
+                          (pcase (org-element-type obj)
+                            ((or 'citation 'citation-reference) obj)
+                            (_ (user-error "Wrong object type")))))))
+
 
 ;;; "Insert" capability
 (defun org-cite-basic--complete-style (_)
@@ -920,7 +970,7 @@ Raise an error when no bibliography is set in the buffer."
   :activate #'org-cite-basic-activate
   :export-citation #'org-cite-basic-export-citation
   :export-bibliography #'org-cite-basic-export-bibliography
-  :follow #'org-cite-basic-goto
+  :follow #'org-cite-basic-follow
   :insert (org-cite-make-insert-processor #'org-cite-basic--complete-key
                                           #'org-cite-basic--complete-style)
   :cite-styles
diff --git a/lisp/om.el b/lisp/om.el
new file mode 100644
index 000000000..5333407bc
--- /dev/null
+++ b/lisp/om.el
@@ -0,0 +1,299 @@
+;;; om.el --- Org Menu library                  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Tor-björn Claesson <tclaes...@gmail.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library provides facilities for displaying menus in org mode.
+
+;;; Code:
+
+(require 'cl-macs)
+(require 'org-macs)
+(require 'transient)
+(require 'which-key)
+
+(org-assert-version)
+
+
+;;; Configuration variables
+(defgroup org-menu nil
+  "Options concerning menus in Org mode."
+  :group 'org
+  :tag "Org Menu")
+
+(defcustom org-menu-switch '-
+  "Prefix argument for inverting the behaviour of Org menus with regards to
+`org-menu-mode'."
+  :group 'org-menu
+  :package-version '(Org . "9.8")
+  :type 'sexp)
+
+(defcustom org-menu-system 'transient
+  "The menu system to use for displaying Org Menus.
+
+Unless equal to transient, it should be a function with the
+signature (actions arguments), where actions is a customization with
+the same form as `org-cite-basic-follow-actions' and arguments is an
+hash table where the key is the ! prefixed name of the argument and
+the value it's value."
+  :group 'org-menu
+  :package-version '(Org . "9.8")
+  :type 'sexp)
+
+
+;;; Minor mode
+(define-minor-mode org-menu-mode
+  "Org menu mode.
+When Org menu mode is enabled, a menu prompting the user for an action
+will be presented upon activating certain objects.
+
+New menus can be defined using `org-menu-define'.
+
+The menu system used can be customized in `org-menu-system'.
+
+When org-menu-mode is active, it can be transiently deactivated by the 
+prefix argument specified in `org-menu-switch', and vice verse transiently
+activated when inactive."
+  :init-value nil
+  :lighter " OM")
+
+;;; Helper functions
+(cl-defmacro org-menu--with-arguments (arg-list &body body)
+  "Makes the arguments, prefixed with !, visible to BODY."
+  `(dlet ,(mapcar (lambda (arg)
+                    `(,(intern (concat "!" (symbol-name arg))) ,arg))
+                  arg-list)
+     ,@body))
+
+(defun org-menu--actions-to-keymap (actions arg-list)
+  "Given ACTIONS (on the form of `org-cite-basic-follow-actions')
+and ARG-LIST (a hash table, whith keys and values to bind for the
+actions in ACTIONS), return a keymap with those bidings."
+  (let ((new-map (make-sparse-keymap)))
+    (cl-loop
+     for group across actions
+     do (cl-loop
+         for specification across group
+         do
+         (pcase specification
+           (`(,key ,desc (,fn . ,fn-args) . ,_)
+            (apply #'keymap-set
+                   (list new-map
+                     key
+                     `(,desc .
+                             ((dlet ,(mapcar (lambda (binding)
+                                               `(,(car binding)
+                                                 ,(cadr binding)))
+                                             arg-list)
+                                (interactive)
+                                (,fn ,@fn-args))))))))))
+    new-map))
+
+(defun org-menu-which-key (actions arg-list)
+  "Show an org-menu using which-key.
+
+This function is a valid value for `org-menu-system'."
+  (interactive)
+  (which-key--show-keymap "hepp"
+                          (org-menu--actions-to-keymap
+                           actions arg-list)
+                          nil t))
+
+(defun org-menu--setup-children (name actions arglist)
+  "Populate the menu of an org-menu transient."
+  (transient-parse-suffixes
+   name
+   (cl-map
+    'vector
+    (lambda (group)
+      (cl-map
+       'vector
+       (lambda (specification)
+         (org-menu--wrap-specification specification `(,@arglist)))
+       group))
+    (eval actions))))
+
+(defun org-menu--wrap-specification (specification arg-list)
+  "Handle special syntax for `org-cite-basic-follow-actions'.
+
+In addition to the syntax described in 
+`(transient)Binding Suffix and Infix Commands', the names in arg-list,
+prefixed by `!', can be used to access those arguments."
+  (pcase specification
+    (`(,key ,desc (lambda ,args . ,fn-args) . ,other)
+     `(,key ,desc
+            (lambda ,args
+              ,(unless (and (listp (car fn-args))
+                            (equal (caar fn-args)
+                                   'interactive))
+                 '(interactive))
+              (let ,(mapcar
+                     (lambda (arg)
+                       `(,(intern (concat "!" (symbol-name arg)))
+                         (plist-get (transient-scope)
+                                    ,(intern (concat ":" (symbol-name arg))))))
+                     arg-list))
+              ,@fn-args)
+            ,@other))
+    (`(,key ,desc (,fn . ,fn-args) . ,other)
+     `(,key ,desc
+            (lambda ()
+	      (interactive)
+              (let ,(mapcar
+                     (lambda (arg)
+                       `(,(intern (concat "!" (symbol-name arg)))
+                         (plist-get (transient-scope)
+                                    ,(intern (concat ":" (symbol-name arg))))))
+                     arg-list)
+	        (,fn ,@fn-args)))
+            ,@other))
+    (other other)))
+
+
+;;; Main macro definition
+(cl-defmacro org-menu-define (name
+                              arglist
+                              docstring
+                              contents
+                              default-action
+                              &body body)
+  "Define an org menu.
+
+A function called NAME will be created to activate the menu.
+
+ARGLIST is the name of the arguments given to this function. 
+Unless it ends in prefix-argument, this will be appended.
+
+CONTENTS is the contents of the menu. It follows the syntax
+decribed in `(transient)Binding Suffix and Infix Commands',
+with the addition that the arguments in ARGLIST are accessible
+prefixed with !. For an example, see `org-cite-basic-follow'.
+
+DEFAULT-ACTION specifies the action to be taken, if org-menu is
+inactive (as determined by `org-menu-mode' and modified by a
+prefix argument set in `org-menu-switch'.
+It has the form of a function call, where the arguments in 
+ARGLIST are accessible prefixed by !. For example, the default action 
+of `org-cite-basic-follow', which is defined with n ARGLIST 
+(citation-object prefix-argument), has the form
+(org-cite-basic-goto !citation-object !prefix-argument).
+
+BODY is optional and can be used to set up the interactive
+environemnt and validate arguments."
+  (declare (indent defun))
+  (let ((menu-default-actions-name
+         (intern
+          (concat (symbol-name name)
+                  "-default-action")))
+        (menu-setup-children-name
+         (intern
+          (concat (symbol-name name)
+                  "--setup-children")))                
+        (menu-actions
+         (intern
+          (concat (symbol-name name)
+                  "-actions")))
+        (complete-arglist (if (member 'prefix-argument arglist)
+                              arglist
+                            `(,@arglist prefix-argument))))
+    `(progn
+       (defcustom ,menu-actions ,contents
+         ,(concat "Actions in the `"
+                  (symbol-name name)
+                  "' org menu.
+
+This option uses the same syntax as `transient-define-prefix', see Info node
+`(transient)Binding Suffix and Infix Commands'. In addition, it is possible
+to specify a function call for the COMMAND part, where ARGUMENTS can be used
+to access those values.
+
+For example: 
+
+[[\"Open\" (\"b\" \"bibliography entry\" 
+(org-cite-basic-goto !citation-object !prefix-argument))]]
+
+will create an entry labeled \"bibliography entry\", activated with the 
+b key, that calls org-cite-basic-goto with citation-object and
+prefix-argument as arguments.")
+         :group 'org-menu
+         :type 'sexp)
+       
+       (defcustom ,menu-default-actions-name ',default-action
+         ,(concat "Default action for `"
+                  (symbol-name name)
+                  "'.
+This should be a function accepting the arguments
+"
+                  (prin1-to-string complete-arglist)
+                  ".")
+         :group 'org-menu
+         :type 'sexp)
+
+       (defun ,menu-setup-children-name (_)
+         ,(concat "Minimal wrapper to setup " (symbol-name name) ".")
+         (org-menu--setup-children ',name ',menu-actions ',complete-arglist))
+       
+       (transient-define-prefix
+         ,name (,@arglist &optional prefix-argument)
+         ,docstring
+         [:class
+          transient-columns
+          :setup-children
+          ,menu-setup-children-name
+          :pad-keys t]
+         ;; Make sure we have an interactive body
+         ,@(pcase body
+             (`((interactive . ,interactive-spec) . ,body)
+              `((interactive ,@interactive-spec) ,body))
+             (_
+              `((interactive (list (org-element-context))) ,@body)))
+         ;; Should we display a menu? If so, how?
+         (cond ((not (xor org-menu-mode
+                          (eq prefix-argument org-menu-switch)))
+                ;; Call the default action
+                (org-menu--with-arguments
+                 ,complete-arglist
+                 (eval ,menu-default-actions-name)))
+               ((eq org-menu-system 'transient)
+                ;; Activate transient
+                (transient-setup
+                 (quote ,name) nil nil
+                 :scope (list
+                         ,@(mapcan (lambda (parameter)
+                                     (list  (intern
+                                             (concat ":"
+                                                     (symbol-name parameter)))
+                                            parameter))
+                                   complete-arglist))))
+               (t
+                ;; Use the system specified in `org-menu-system'
+                (funcall
+                 org-menu-system
+                 ,menu-actions
+                 (list ,@(cl-mapcar (lambda (key value)
+                                      `(list
+                                       ',(intern (concat "!" (symbol-name key)))
+                                       ,value))
+                                   complete-arglist
+                                   complete-arglist)))))))))
+
+(provide 'om)
+;;; om.el ends here
-- 
2.47.2

Reply via email to