Today I’d like to learn – and then show – how to define a user option in Emacs so that setting it will trigger evaluating some Elisp code. Here is my use-case. I want the option to be a timestamp. The most convenient way to store it is as Unix time (number of seconds since the epoch), since the code using it will perform comparisons between that timestamp and other times. On the other hand, the most convenient way to set it is as ISO-8601 timestamp, which is inifinitely more human-readable than the Unix time.
It turns out that reading the relevant portion of the manual is not an easy task. I think I get the main idea, but I’m still not sure I understand the difference between :set
and :initialize
very well, and the fact that the manual says “You have to really understand the workings of Custom to use ‘:get’ correctly” scares me a bit. My first thought was to use :set
to convert the ISO-8601 timestamp to Unix time, and :get
to convert back, but I’m not sure if this is “using :get correctly”. Well, I asked on the Emacs mailing list, and soon got an answer from Eli himself. I have to admit that I still do not understand everything here, but at least I feel a bit more confident now that :initialize
is not what I need here.
So, here is my code. It turned out that the hard portion is actually parsing ISO-8601. Who would have guessed? Emacs has an in-built library for that, but it lacks a function to just parse a string like
2025-05-09
into a proper Elisp time object. The iso8601-parse
does almost that, but instead of filling the missing data (the hour, minute and second) with zeroes, it uses nils. That sort of makes sense, but then encode-time
does not like it at all. Good thing is that Elisp has another function which does exactly what I need here, that is, turns those nils into zeros – decoded-time-set-defaults
.
(require 'iso8601) (defcustom timestamp-user-option "1970-01-01T00:00+00" "An example of a timestamp user option. This is stored as a number but set in Customize as ISO-8601 string." :type '(string :match (lambda (widget val) (iso8601-valid-p val))) :set (lambda (sym val) (set sym (float-time (encode-time (decoded-time-set-defaults (iso8601-parse val)))))) :get (lambda (sym) (format-time-string "%FT%T%:::z" (seconds-to-time (symbol-value sym)))))
Now you can use customize-option
to set this to (for example) 2025-05-19, and the real value of the variable will be 1747605600.0 (at least in my timezone).
One little disadvantage is that when you enter the Customize UI again, you’ll see the full ISO-8601 timestamp, that is, 2025-05-19T00:00:00+02
(again, in my timezone). This could be remedied by changing this variable into a structure (maybe a plist, maybe an alist, maybe an object) and store both the actual timestamp entered by the user and the result of the conversion to a number, but I don’t think it’s worth the hassle, since then you wouldn’t be able to treat this variable as a number – you’d have to use plist-get
or some other accessor function. Another idea could be to use timestamp-user-option
to store the exact string the user typed and some other variable, say timestamp-user-option-internal
to store its numerical representation. This would probably work, but I think it’s even worse, since then both variables can get out of sync with a setq
or something.
Anyway, that’s it for today – but expect a follow-up, where we will actually use a user option like this, in the near future!