From 4f67a033fd547e8bcf6a35520c9b0df367de9ed6 Mon Sep 17 00:00:00 2001 From: Jonas Bernoulli Date: Tue, 24 Oct 2023 16:50:11 +0200 Subject: [PATCH] Support display-only information in place of a suffix Group headings are also display-only information and raw strings can be used anywhere a suffix can appear, but this class makes it possible to display arbitrary information anywhere *and* to style it in any way one fancies. By inheriting from `transient-suffix' (instead of `transient-child') we avoid having to implement many new methods, but semantically this is a bit questionable. Leaving the `command' and `key' slots undefined, means that we have to use `slot-boundp' in a few places, but on the other hand that helps dealing the semantic inconsistency. Closes #226. --- docs/transient.org | 17 +++++++++++++++++ docs/transient.texi | 18 ++++++++++++++++++ lisp/transient.el | 37 ++++++++++++++++++++++++++----------- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/docs/transient.org b/docs/transient.org index 8351af28..1ddc011e 100644 --- a/docs/transient.org +++ b/docs/transient.org @@ -1612,6 +1612,23 @@ object should not affect later invocations. - Classes used for infix commands that represent variables should derived from the abstract ~transient-variable~ class. +- The ~transient-information~ class is special in that suffixes that use + this class are not associated with a command and thus also not with + any key binding. Such suffixes are only used to display arbitrary + information, and that anywhere a suffix can appear. Display-only + suffix specifications take this form: + + #+begin_src emacs-lisp + ([LEVEL] :info DESCRIPTION [KEYWORD VALUE]...) + #+end_src + + The ~:info~ keyword argument replaces the ~:description~ keyword used for + other suffix classes. Other keyword arguments that you might want to + set, include ~:face~, predicate keywords (such as ~:if~), and ~:format~. + By default the value of ~:format~ includes ~%k~, which for this class is + replaced with the empty string or spaces, if keys are being padded in + the containing group. + Magit defines additional classes, which can serve as examples for the fancy things you can do without modifying Transient. Some of these classes will likely get generalized and added to Transient. For now diff --git a/docs/transient.texi b/docs/transient.texi index c20f56d7..b03f540a 100644 --- a/docs/transient.texi +++ b/docs/transient.texi @@ -1841,6 +1841,24 @@ indicates that all remaining arguments are files. @item Classes used for infix commands that represent variables should derived from the abstract @code{transient-variable} class. + +@item +The @code{transient-information} class is special in that suffixes that use +this class are not associated with a command and thus also not with +any key binding. Such suffixes are only used to display arbitrary +information, and that anywhere a suffix can appear. Display-only +suffix specifications take this form: + +@lisp +([LEVEL] :info DESCRIPTION [KEYWORD VALUE]...) +@end lisp + +The @code{:info} keyword argument replaces the @code{:description} keyword used for +other suffix classes. Other keyword arguments that you might want to +set, include @code{:face}, predicate keywords (such as @code{:if}), and @code{:format}. +By default the value of @code{:format} includes @code{%k}, which for this class is +replaced with the empty string or spaces, if keys are being padded in +the containing group. @end itemize Magit defines additional classes, which can serve as examples for the diff --git a/lisp/transient.el b/lisp/transient.el index 83ddefaf..142fe9eb 100644 --- a/lisp/transient.el +++ b/lisp/transient.el @@ -737,6 +737,11 @@ slot is non-nil." :documentation "Inapt if major-mode does not derive from value.")) "Superclass for suffix command.") +(defclass transient-information (transient-suffix) + ((format :initform " %k %d")) + "Display-only information. +A suffix object with no associated command.") + (defclass transient-infix (transient-suffix) ((transient :initform t) (argument :initarg :argument) @@ -1071,8 +1076,9 @@ example, sets a variable, use `transient-define-infix' instead. (commandp (cadr spec))) (setq args (plist-put args :description (macroexp-quote pop))))) (cond + ((eq car :info)) ((keywordp car) - (error "Need command, got `%s'" car)) + (error "Need command or `:info', got `%s'" car)) ((symbolp car) (setq args (plist-put args :command (macroexp-quote pop)))) ((and (commandp car) @@ -1128,6 +1134,9 @@ example, sets a variable, use `transient-define-infix' instead. (val pop)) (cond ((eq key :class) (setq class val)) ((eq key :level) (setq level val)) + ((eq key :info) + (setq class 'transient-information) + (setq args (plist-put args :description val))) ((eq (car-safe val) '\,) (setq args (plist-put args key (cadr val)))) ((or (symbolp val) @@ -1871,6 +1880,7 @@ value. Otherwise return CHILDREN as is." (cl-labels ((s (def) (cond ((stringp def) nil) + ((cl-typep def 'transient-information) nil) ((listp def) (cl-mapcan #'s def)) ((cl-typep def 'transient-group) (cl-mapcan #'s (oref def suffixes))) @@ -1908,12 +1918,15 @@ value. Otherwise return CHILDREN as is." (transient--debug " autoload %s" cmd) (autoload-do-load fn))) (when (transient--use-level-p level) - (unless (and cmd (symbolp cmd)) - (error "BUG: Non-symbolic suffix command: %s" cmd)) - (let ((obj (if-let ((proto (get cmd 'transient--suffix))) - (apply #'clone proto :level level args) - (apply class :command cmd :level level args)))) - (cond ((commandp cmd)) + (let ((obj (if (child-of-class-p class 'transient-information) + (apply class :level level args) + (unless (and cmd (symbolp cmd)) + (error "BUG: Non-symbolic suffix command: %s" cmd)) + (if-let ((proto (and cmd (get cmd 'transient--suffix)))) + (apply #'clone proto :level level args) + (apply class :command cmd :level level args))))) + (cond ((not cmd)) + ((commandp cmd)) ((or (cl-typep obj 'transient-switch) (cl-typep obj 'transient-option)) ;; As a temporary special case, if the package was compiled @@ -1922,7 +1935,8 @@ value. Otherwise return CHILDREN as is." (defalias cmd #'transient--default-infix-command)) ((transient--use-suffix-p obj) (error "Suffix command %s is not defined or autoloaded" cmd))) - (transient--init-suffix-key obj) + (unless (cl-typep obj 'transient-information) + (transient--init-suffix-key obj)) (when (transient--use-suffix-p obj) (if (transient--inapt-suffix-p obj) (oset obj inapt t) @@ -3548,8 +3562,8 @@ Optional support for popup buttons is also implemented here." (cl-defmethod transient-format-key ((obj transient-suffix)) "Format OBJ's `key' for display and return the result." - (let ((key (oref obj key)) - (cmd (oref obj command))) + (let ((key (if (slot-boundp obj 'key) (oref obj key) "")) + (cmd (and (slot-boundp obj 'command) (oref obj command)))) (when-let ((width (oref transient--pending-group pad-keys))) (setq key (truncate-string-to-width key width nil ?\s))) (if transient--redisplay-key @@ -3642,7 +3656,8 @@ If the OBJ's `key' is currently unreachable, then apply the face (funcall (oref transient--prefix suffix-description) obj)) (propertize "(BUG: no description)" 'face 'error)))) - (cond ((transient--key-unreachable-p obj) + (cond ((and (slot-boundp obj 'key) + (transient--key-unreachable-p obj)) (propertize desc 'face 'transient-unreachable)) ((and transient-highlight-higher-levels (> (max (oref obj level) transient--max-group-level)