Subject: Re: bug#79106: [PATCH] Add new optional keywords to use-package

From: Elijah Gabe Pérez <[email-masked]>
Date: Sat, 02 Aug 2025 14:39:09 -0600

Eli Zaretskii <[email-masked]> writes:

>> From: Elijah Gabe Pérez <[email-masked]>
>> Cc: Stéphane Marks <[email-masked]>,  Karthik
>>  Chikmagalur
>>  <[email-masked]>,  [email-masked], John Wiegley
>>  <[email-masked]>
>> Date: Wed, 30 Jul 2025 17:08:32 -0600
>> 
>> Since this discussion (and the emacs-devel discussion) doesn't seem to
>> be leading to any solution (besides the divided opinions), my solution i
>> can think of would be to make the keywords that may be considered useful
>> (I'm not sure which ones) part of use-package, and leave the rest in
>> ELPA (or in NonGNU) in a single package.
>> 
>> Of course, this package was not intended to be part of use-package, just
>> an **optional** thing that i wanted to have.
>
> From my POV, we can install these changes in core, assuming that you
> can post a patch that incorporates the relevant parts of the
> discussions.

Sounds good to me, I've updated the patch for include *only* the
keywords that people may find useful.

The rest can be kept in my use-package-x repository.

> Please also rename the new files such that they don't include the
> "-x-" part.  That is, instead of, e.g., use-package-x-advice.el add a
> file use-package-advice.el.  Also, please put them inside
> lisp/use-package/, not in its subdirectory.

Done:
>From 574b5b00b5138646c0d030100321743cea79225a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?El=C3=ADas=20Gabriel=20P=C3=A9rez?= <[email-masked]>
Date: Sat, 26 Jul 2025 22:43:38 -0600
Subject: [PATCH] Add new optional keywords to use-package.  (Bug#79106)

* etc/NEWS: Announce changes.
* lisp/use-package/use-package-core.el (use-package-keywords):
Add new keywords to keywords list.
(use-package-normalize/:custom-face*)
(use-package-handler/:custom-face*): New functions.
* lisp/use-package/use-package-hook.el:
* lisp/use-package/use-package-local-set.el:
* lisp/use-package/use-package-set.el: New files.
---
 etc/NEWS                                  |  27 +++
 lisp/use-package/use-package-core.el      |  20 +++
 lisp/use-package/use-package-hook.el      | 209 ++++++++++++++++++++++
 lisp/use-package/use-package-local-set.el |  89 +++++++++
 lisp/use-package/use-package-set.el       |  69 +++++++
 5 files changed, 414 insertions(+)
 create mode 100644 lisp/use-package/use-package-hook.el
 create mode 100644 lisp/use-package/use-package-local-set.el
 create mode 100644 lisp/use-package/use-package-set.el

diff --git a/etc/NEWS b/etc/NEWS
index a81d1105443..beb788d3a72 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -955,6 +955,33 @@ units that 'elisp-eldoc-funcall-with-docstring' and
 'elisp-eldoc-var-docstring-with-value' will show.  By default, it is set
 to 1000 characters.
 
+** Use package
+
++++
+*** New ':hook-suffix' keyword.
+This keyword enables the user to change the
+'use-package-hook-name-suffix' to the use-package declaration where this
+is used.
+
++++
+*** New ':set' keyword.
+This keyword enables the user to bind plain variables.
+
++++
+*** New ':local-set' keyword.
+This keyword is similar to ':set', but bind the variables locally to the
+package hook or to a list of hooks.
+
++++
+*** New ':custom-face*' keyword.
+This keyword is similar to ':custom-face*', but overrides the face
+specs.
+
++++
+*** New ':hook+' keyword.
+This keyword is similar to ':hook', but supports hook depths and set
+multiple function into a hook or for each hook.
+
 ** Buffer Menu
 
 ---
diff --git a/lisp/use-package/use-package-core.el b/lisp/use-package/use-package-core.el
index 65b9485b4e3..2dae6e0c83d 100644
--- a/lisp/use-package/use-package-core.el
+++ b/lisp/use-package/use-package-core.el
@@ -77,13 +77,17 @@ use-package-keywords
     :defines
     :functions
     :preface
+    :hook-suffix
     :if :when :unless
     :vc
     :no-require
     :catch
     :after
+    :set
+    :local-set
     :custom
     :custom-face
+    :custom-face*
     :bind
     :bind*
     :bind-keymap
@@ -93,6 +97,7 @@ use-package-keywords
     :magic
     :magic-fallback
     :hook
+    :hook+
     ;; Any other keyword that also declares commands to be autoloaded (such as
     ;; :bind) must appear before this keyword.
     :commands
@@ -1630,6 +1635,21 @@ use-package-handler/:custom-face
            args)
    (use-package-process-keywords name rest state)))
 
+;;;; :custom-face*
+;; see: bug#77928
+
+(defalias 'use-package-normalize/:custom-face* 'use-package-normalize/:custom-face)
+
+(defun use-package-handler/:custom-face* (name _keyword args rest state)
+  "Like :custom-face*, but override the face specs."
+  (use-package-concat
+   (mapcar #'(lambda (def)
+               `(progn
+                  (apply #'face-spec-set (backquote ,def))
+                  (put ',(car def) 'face-modified t)))
+           args)
+   (use-package-process-keywords name rest state)))
+
 ;;;; :init
 
 (defalias 'use-package-normalize/:init 'use-package-normalize-forms)
diff --git a/lisp/use-package/use-package-hook.el b/lisp/use-package/use-package-hook.el
new file mode 100644
index 00000000000..10c04e17d89
--- /dev/null
+++ b/lisp/use-package/use-package-hook.el
@@ -0,0 +1,209 @@
+;;; use-package-hook.el --- :hook+ :hook-suffix keywords definitions  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Elias G. Perez <[email-masked]>
+;; Keywords: convenience, tools, extensions
+;; Package-Requires: ((use-package "2.1"))
+
+;; This program 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.
+
+;; This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file provides the following extra keywords for
+;; `use-package':
+;;
+;; * :hook-suffix
+;;   Change `use-package-hook-name-suffix' value to only the use-package
+;;   declaration.
+;;
+;; * :hook+
+;;   An enchanted :hook which supports hooks depths and
+;;   set multiple functions.
+;;
+;;   The hook depth is provided using the sub-keyword :depth
+;;   e.g.
+;;     :hook+
+;;     (:depth 10
+;;       (major-mode . my-func))
+;;
+;;   To set multiple functions (including lambdas) to the
+;;   hook or list of hooks you can use the :multi keyword:
+;;
+;;   :hook (my-hook-or-list-of-hooks
+;;           . (:multi fn1 fn2 (lambda () ..) ...))
+;;
+;;   This also supports the :hook valid forms:
+;;
+;;     :hook+
+;;     (:depth 10
+;;       (major-mode . my-func-or-lambda)
+;;       (mode mode2 mode3)
+;;       single-mode)
+;;     ((hook1 hook2) . (:multi fn1 fn2 fn3))
+;;     ...and also use it as a normal :hook...
+;;     (major-mode . my-func-or-lambda)
+;;     (mode mode2 mode3)
+;;     single-mode
+;;
+
+;;; Code:
+
+;;; Requires
+(require 'use-package-core)
+
+
+
+;;; Functions
+
+;;; :hook-suffix
+;;;###autoload
+(defun use-package-normalize/:hook-suffix (_name keyword args)
+  "Normalize :hook-prefix, ensure it's a string or nil."
+  (use-package-only-one (symbol-name keyword) args
+    (lambda (label arg)
+      (unless (or (stringp arg) (null arg))
+        (use-package-error
+         (concat label " must be nil or a string")))
+      arg)))
+
+;;;###autoload
+(defun use-package-handler/:hook-suffix (name _keyword arg rest state)
+  "Normalize :hook-prefix.
+Set `use-package-hook-name-suffix' to ARG only in the current
+`use-package' declaration."
+  (dlet ((use-package-hook-name-suffix arg))
+    (use-package-process-keywords name rest state)))
+
+
+
+;;; :hook+
+(defun use-package-hook--normalize-pairs (list label name)
+  "Normalize all the pairs in the LIST."
+  (if (ignore-errors (eq (cadar list) :multi))
+      ;; FIXME: There is not a better way to include this into the
+      ;; loop (below), so just return the LIST without normalizing.
+      list
+    (use-package-normalize-pairs
+     (lambda (k)
+       (or (use-package-non-nil-symbolp k)
+           (and k (let ((every t))
+                    (while (and every k)
+                      (if (and (consp k)
+                               (use-package-non-nil-symbolp (car k)))
+                          (setq k (cdr k))
+                        (setq every nil)))
+                    every))))
+     #'use-package-recognize-function
+     (if (string-suffix-p "-mode" (symbol-name name))
+         name
+       (intern (concat (symbol-name name) "-mode")))
+     label list)))
+
+;;;###autoload
+(defun use-package-normalize/:hook+ (name keyword args)
+  "Normalize :hook+ keyword.
+Return the proper list or ARGS for `use-package-autoloads/:hook+'
+and `use-package-handler/:hook+'."
+  (use-package-as-one (symbol-name keyword) args
+    (lambda (label arg)
+      (unless (or (use-package-non-nil-symbolp arg) (consp arg))
+        (use-package-error
+         (concat label
+                 " must be"
+                 " a <symbol> or a list or these"
+                 " or (<symbol or list of symbols> . <function>)"
+                 " or (<symbol or list of symbols> . (:multi <functions> ...))"
+                 " or (:depth <depth number> <any of previous forms>...)")))
+      ;; Check if :depth is defined and return (<n-depth>
+      ;; <the-hook-form>) for the handler function, otherwise just
+      ;; return the normal list
+      (mapcan
+       (lambda (elt)
+         (if (eq (car-safe elt) :depth)
+             (mapcar (lambda (pairs)
+                       (cons (nth 1 elt) (list pairs)))
+                     (use-package-hook--normalize-pairs (cddr elt) label name))
+           (use-package-hook--normalize-pairs (list elt) label name)))
+       args))))
+
+(defun use-package-hook--normalize-commands (list)
+  "Like `use-package-normalize-commands' but for supporting the :depth keyword."
+  (mapcar (lambda (x)
+            (cond ((numberp (car x))
+                   (cons (car x) (use-package-normalize-commands (cdr x))))
+                  ((consp x)
+                   (cons (car x) (use-package-normalize-function (cdr x))))
+                  (t x)))
+          list))
+
+(defun use-package-autoloads/:hook+ (_name _keyword args)
+  "Like `use-package-autoloads-mode' but supports the :depth and :multi keywords."
+  (setq args
+        (mapcar
+         (lambda (list)
+           (cond
+            ;; return the list without the depth number
+            ((integerp (car list)) (cadr list))
+            (t list)))
+         args))
+
+  (cl-loop for x in args
+           for multi = (ignore-errors (eq (cadr x) :multi))
+           when (and (consp x) (or (use-package-non-nil-symbolp (cdr x)) multi))
+           if multi append
+           (cl-loop for cm in (cddr x)
+                    if (use-package-non-nil-symbolp cm)
+                    collect (cons cm 'command))
+           else collect (cons (cdr x) 'command)))
+
+(defun use-package-hook--create-hook (sym fun depth)
+  "Return the proper `add-hook' for mode SYM with FUN and DEPTH (if there is)."
+  (let ((symname (symbol-name sym)))
+    (if (and (boundp sym)
+             (not (string-suffix-p "-mode" symname)))
+        `(add-hook (quote ,sym) (function ,fun) ,depth)
+      `(add-hook
+        (quote ,(intern
+                 (concat symname use-package-hook-name-suffix)))
+        (function ,fun)
+        ,depth))))
+
+;;;###autoload
+(defun use-package-handler/:hook+ (name _keyword args rest state)
+  "Hadle :hook+ keyword.
+Add the proper `add-hook' to use-package expanded form,
+compared to normal :hook, this handle hook depths and multiple
+functions."
+  (use-package-concat
+   (use-package-process-keywords name rest state)
+   (cl-mapcan
+    (lambda (def)
+      (let* ((car (car def))
+             (depth (if (integerp car) car))
+             (syms (if depth (caadr def) car))
+             (fun (if depth (cdadr def) (cdr def)))
+             (multi-p (and (listp fun) (eq (car fun) :multi))))
+        (when fun
+          (cl-loop
+           for mode in (use-package-hook-handler-normalize-mode-symbols syms)
+           if multi-p append ; For `:multi'
+           (cl-loop for fn in (cdr fun) collect
+                    (use-package-hook--create-hook mode fn depth))
+           else
+           collect (use-package-hook--create-hook mode fun depth)))))
+    (use-package-hook--normalize-commands args))))
+
+(provide 'use-package-hook)
+;;; use-package-hook.el ends here
diff --git a/lisp/use-package/use-package-local-set.el b/lisp/use-package/use-package-local-set.el
new file mode 100644
index 00000000000..dfdbb7d876c
--- /dev/null
+++ b/lisp/use-package/use-package-local-set.el
@@ -0,0 +1,89 @@
+;;; use-package-local-set.el --- :local-set keyword definition  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Elias G. Perez <[email-masked]>
+;; Keywords: convenience, tools, extensions
+;; Package-Requires: ((use-package "2.1"))
+
+;; This program 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.
+
+;; This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file provides the following extra keyword for
+;; `use-package':
+;;
+;; * :local-set
+;;
+;;   Set variables locally when entering to a mode.
+;;
+;;     :local-set
+;;     (<variable> <value> ...) <- Automatically set to package mode hook
+;;     --or--
+;;     (:hook <hook-or-list-of-hooks> <- Set it only to hook(s),
+;;       <variable> <value>              compared to :hook and :hook+
+;;       ...)                            you need to add the full hook name
+;;
+
+;;; Code:
+
+;;; Requires
+(require 'use-package-core)
+
+
+
+;;;###autoload
+(defun use-package-normalize/:local-set (_name keyword args)
+  "Normalize :local-set keyword, ensure ARGS are valid."
+  (use-package-as-one (symbol-name keyword) args
+    (lambda (label arg)
+      (let ((arg-car (car arg)))
+        (unless (or (eq arg-car :hook) (consp arg-car) (use-package-non-nil-symbolp arg-car))
+          (use-package-error
+           (concat label
+                   " can be (<symbol> <value>...)"
+                   " or (:hook <symbol-hook> <symbol> <value> ...)")))
+        args))))
+
+(defun use-package-local-set--local-hook (hook values)
+  `(add-hook (quote ,hook)
+             (lambda (&rest _)
+               (setq-local ,@values))))
+
+;;;###autoload
+(defun use-package-handler/:local-set (name _keyword args rest state)
+  (use-package-concat
+   (mapcan
+    (lambda (elt)
+      (if (eq (car elt) :hook)
+          ;; :hook
+          (if-let* ((hook (nth 1 elt))
+                    ((listp hook)))
+              (mapcar
+               (lambda (x) (use-package-local-set--local-hook x (cddr elt)))
+               hook)
+            (list (use-package-local-set--local-hook hook (cddr elt))))
+        ;; plain variables
+        (let* ((sym-name (symbol-name name))
+               (hook (intern (concat
+                              sym-name
+                              (if (string-suffix-p "-mode" sym-name)
+                                  "-hook"
+                                "-mode-hook")))))
+          (list (use-package-local-set--local-hook hook elt)))))
+    args)
+   (use-package-process-keywords name rest state)))
+
+(provide 'use-package-local-set)
+;;; use-package-local-set.el ends here
diff --git a/lisp/use-package/use-package-set.el b/lisp/use-package/use-package-set.el
new file mode 100644
index 00000000000..b86544d311a
--- /dev/null
+++ b/lisp/use-package/use-package-set.el
@@ -0,0 +1,69 @@
+;;; use-package-set.el --- :set keyword definition  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Elias G. Perez <[email-masked]>
+;; Keywords: convenience, tools, extensions
+;; Package-Requires: ((use-package "2.1"))
+
+;; This program 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.
+
+;; This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file provides the following extra keyword for
+;; `use-package':
+;;
+;; * :set
+;;   Similar to :custom, but is meant to bind plain variables.
+;;   This uses setq-default for bind the variables.
+;;
+;;     :set (<variable> [value]) ...
+;;
+;;   VALUE is optional, if omitted, VARIABLE will be set to nil.
+;;
+
+;;; Code:
+
+;;; Requires
+(require 'use-package)
+
+
+
+;;; Functions
+
+;;;###autoload
+(defun use-package-normalize/:set (_name keyword args)
+  "Normalize :setopt keyword, ensure the values in ARGS are valid."
+  (use-package-as-one (symbol-name keyword) args
+    (lambda (label arg)
+      (unless (listp arg)
+        (use-package-error
+         (concat label
+                 " must be a (<symbol> [value])"
+                 " or a list of these")))
+      (if (use-package-non-nil-symbolp (car arg))
+          (list arg)
+        arg))))
+
+;;;###autoload
+(defun use-package-handler/:set (name _keyword args rest state)
+  (use-package-concat
+   `((setq-default
+      ,@(mapcan
+         (lambda (list) (list (car list) (nth 1 list)))
+         args)))
+   (use-package-process-keywords name rest state)))
+
+(provide 'use-package-set)
+;;; use-package-set.el ends here
-- 
2.50.1

NOTE: The patch does not yet include the corresponding change in the
use-package manual, I am waiting for feedback first.
-- 
- E.G via Gnus and Org.