I've recently also been dealing with this problem and have come up with a working solution.
I initially uploaded it to reddit, read custom-set-variables
and custom file trampling for all the details.
I've got the solution in place in my emacs configuration and it is working perfectly. Simply installing a package from melpa no longer clobbers my custom file with the custom-set-variable
declarations from my config file.
It is "a" solution, not "the" solution. It involves 2 vars and 2 pieces of advice. But the solution is, actually, rather "simple" and maybe can point the way for emacs maintainers to provide a similar mechanism out of the box in the future.
A brief summary of solution:
First, I summarize here the 3 ways to set custom variables properly, with a note on their state as regards this solution to the problem:
custom-set-variables
: good
customize-set-variable
: bad (explained later)
use-package :custom
section: bad (this uses customize-set-variable
)
My solution involves 2 variables (1 is optional, simply used to wrap advices in if
s if you want to), and 2 pieces of advice.
The problem is that the custom system is actually quite good and works
entirely as expected EXCEPT they give the user no option to tell emacs
to "hey, don't write this out to the custom-file, I'm taking care of
it in my config file."
Everything boils down to the following when
statement (in the
custom-save-variables
function in cus-edit.el):
;; Is there anything customized about this variable?
(when (or (and spec (eq (car spec) 'user))
comment
(and (null spec) (get symbol 'saved-value)))
If this evaluates to true then this custom var is written/dumped to
custom-file. We can hitchhike on comment and construct a boolean with it. And so my solution uses comment to fix the entire problem.
Here are the two variable declarations, the first is optional, the
second required. The first is used to construct an if
in the advice if you so desire:
(defvar juus/customize-pinned-use t
"Do =NOT= write values of custom variables using `juus/customize-pinned-string'.
When this value is t and `juus/customize-pinned-string' is a prefix or suffix of
the customization variables' comment then it will not be written out to `custom-file'.
If you set this to nil, values will be written to the `custom-file' and will
then override your settings in your configuration files since the `custom-file' is
loaded after your configuration settings. This can be quite confusing and annoying.
So I recommend to not set this to nil unless you are testing or want to copy a formatted
variable for use in your config file. In that case I highly recommend that you temporarily
setq your `custom-file' to a dump file.")
(defvar juus/customize-pinned-string ":PINNED:"
"The string used to control writing to `custom-file'.
See `juus/customize-pinned-use'.")
Now, if we send in a custom variable that
has a comment that looks like:
":PINNED:"
or ":PINNED: my comment"
or "my comment :PINNED:"
...we have a boolean we can use.
To construct the :around
advice one must copy the entire custom-save-variables
function from cus-edit.el. Wrap the advice in an if
if you want to use the boolean var given above. Then change the when
from above to:
;; Is there anything customized about this variable?
(when (and
(or (and spec (eq (car spec) 'user))
comment
(and (null spec) (get symbol 'saved-value)))
(not
(or
(string-prefix-p juus/customize-pinned-string comment t)
(string-suffix-p juus/customize-pinned-string comment t))))
and now add the advice:
(advice-add 'custom-save-variables :around #'custom-save-variables--whatever-you-named-it)
Now in your config file use this construction for custom-set-variables
(the 3rd nil parameter is problematic [see later] but can be set to t
instead, and sometimes needs to be):
(custom-set-variables
'(cust-var cust-val nil nil ":PINNED: my comment")
)
You can construct a "helper" function to help automate this (an example in link above) but then you will need to use backtick and comma grammar.
This works completely for custom-set-variables
, but not for customize-set-variable
which is what use-package :custom
uses. The reason it doesn't work is the "state" that it saves it in will not pass on the comment to the system. Without a comment we cannot filter it.
So another piece of advice is needed for use-package
, which tells it to use custom-set-variables
instead, and it will automate the :PINNED:
for us, meaning that there will be no need to edit :custom
sections.
The original controlling line in use-package-handler/:custom function in use-package is this:
`(customize-set-variable (quote ,variable) ,value ,comment)))
Replaced with this fixes the issue and automatically prepends ":PINNED:"
to the comment:
(setq comment (concat juus/customize-pinned-string " " comment))
`(custom-set-variables '(,variable ,value ,t ,nil ,comment))))
You'll see that I've used ,t
as the 3rd param. This is because due to
some internal logic in cus-edit.el involving "rogue" variables the
state is set to something other than "Saved and set" which is required
for the comment to be passed on, as far as I can see. The ,t was
required on my computer to fix a small portion of :custom
's that
weren't responding correctly.
Here is the optional use-package
advice. I've used nth
a lot, but let
s could be used to make it more readable, the original params the function expects are in the document line:
(defun use-package-handler/:custom-juus/advice (orig-fun &rest args)
"ADVICE for `use-package-handler/:custom'.
orig-fun expects: name _keyword args rest state"
(if (not juus/customize-pinned-use)
(apply orig-fun args)
(use-package-concat
(mapcar
#'(lambda (def)
(let ((variable (nth 0 def))
(value (nth 1 def))
(comment (nth 2 def)))
(unless (and comment (stringp comment))
(setq comment (format "set by use-package '%s'" (nth 0 args))))
(setq comment (concat juus/customize-pinned-string " " comment))
`(custom-set-variables '(,variable ,value ,t ,nil ,comment))))
(nth 2 args))
(use-package-process-keywords (nth 0 args) (nth 3 args) (nth 4 args)))))
Then add the advice to the system:
(advice-add 'use-package-handler/:custom :around #'use-package-handler/:custom-juus/advice)
CAVEATS:
The functions I copied come from emacs 26.3, so don't use my constructions literally maybe unless you are using 26.3, copy from your source code! As you can see this is a real "hands on" fix.
I have not (yet) looked at the custom-set-faces
side of the
custom-file because I've not used it yet. Maybe someone out there is interested to let me know if a similar problem exists for custom-set-faces
and, if so, if a similar solution works???
Don't use customize-set-variable
, it simply won't work in this system. Convert them to custom-set-variable
declarations.
custom-set-variables
. Do you also consider it more of a bother to usecustomize-set-variable' or
customize-set-value` than to usesetq
? (If so, why?)setq
to be more straightforward, especially wheneval-when-compile
starts getting involved. If I want to set multiple variables in one go, I need to construct quoted lists around my variables and values (which geteval
'd). I haven't triedcustomize-set-variable
, partly because it can't set multiple variables in one call. I didn't know aboutcustomize-set-value
. I don't think I understand the difference between that andcustomize-set-variable
, even after reading the docs.setq
when dealing with custom variables that don't make use of special setters or initialization code (which seems to be most of them)?setq
if there are no setters etc. Most abuses ofsetq
for options, which can get people into trouble, come from complete ignorance of the possibility of setters etc., not from knowing about them and not worrying about them. IOW, people just get in the habit of usingsetq
, and most of the time they are not surprised.