aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-08-24 22:26:33 -0600
committerCraig Jennings <c@cjennings.net>2026-08-24 22:26:33 -0600
commit37e1c94d523e1d9b96ac8d8acbb488636f31c2cc (patch)
tree8b9cdf7950cfa5c6fdd6b371db40dc8caef6ed4b
parent8b461ecd3d4fae3be0a93523d19932e0a3d21c6c (diff)
downloademacs-wttrin-37e1c94d523e1d9b96ac8d8acbb488636f31c2cc.tar.gz
emacs-wttrin-37e1c94d523e1d9b96ac8d8acbb488636f31c2cc.zip
fix: re-read the state file when its path is set after loadrelease/0.4.0
The load-time restore read `wttrin-state-file` at whatever path the option held when wttrin loaded. A `setopt` in a use-package `:config` block runs after that read, so `d` wrote the new file and the next start never opened it. The default reverted to the init value on every restart. I gave the option a `:set` handler that stores the value and re-runs `wttrin--state-load` from the new path. `setopt`, `customize-set-variable`, and Customize all route through it. When the restore changes the effective favorite while the mode-line mode is on, the mode-line refreshes to the restored city. A plain `setq` after load still waits for the next restore, and the docstring now says so.
-rw-r--r--tests/test-wttrin-state-file.el118
-rw-r--r--wttrin.el33
2 files changed, 147 insertions, 4 deletions
diff --git a/tests/test-wttrin-state-file.el b/tests/test-wttrin-state-file.el
index 5648a13..34f8eb3 100644
--- a/tests/test-wttrin-state-file.el
+++ b/tests/test-wttrin-state-file.el
@@ -254,5 +254,123 @@ vars; only the scrub-tolerant search history stays with savehist."
(should-not (memq 'wttrin--saved-locations-runtime
savehist-additional-variables))))
+;;; --------------------------------------------------------------------------
+;;; Path customization after load (`:set' on `wttrin-state-file')
+;;; --------------------------------------------------------------------------
+
+(defmacro test-wttrin-state-file--with-path-sandbox (&rest body)
+ "Run BODY with a fresh temp PATH bound and the state vars isolated.
+`wttrin-state-file' is let-bound to itself so a `customize-set-variable'
+inside BODY is undone on exit; the `customized-value' property and the temp
+file are cleaned up afterward."
+ (declare (indent 0))
+ `(let ((path (expand-file-name
+ (format "wttrin-test-state-%s.el" (random 1000000))
+ temporary-file-directory))
+ (wttrin-state-file wttrin-state-file)
+ (wttrin--favorite-override nil)
+ (wttrin--saved-locations-runtime nil))
+ (unwind-protect
+ (progn ,@body)
+ (put 'wttrin-state-file 'customized-value nil)
+ (when (file-exists-p path)
+ (delete-file path)))))
+
+(ert-deftest test-wttrin-state-file-normal-set-property-wired ()
+ "Normal: `wttrin-state-file' carries the `:set' handler, so `setopt',
+`customize-set-variable', and Customize all route through it."
+ (should (eq (get 'wttrin-state-file 'custom-set) #'wttrin--state-file-set)))
+
+(ert-deftest test-wttrin-state-file-normal-set-after-load-restores-from-new-path ()
+ "Normal: customizing the path after load restores state from the new file.
+This is the use-package `:config' shape — `(setopt wttrin-state-file ...)'
+after `(require 'wttrin)' — where the load-time restore has already read the
+default path and would otherwise never see the file the setter writes."
+ (test-wttrin-state-file--with-path-sandbox
+ (let ((wttrin-state-file path)
+ (wttrin--favorite-override "Salt Lake City, Utah")
+ (wttrin--saved-locations-runtime '(("SLC" . "40.65,-111.87"))))
+ (wttrin--state-save))
+ (customize-set-variable 'wttrin-state-file path)
+ (should (equal wttrin-state-file path))
+ (should (equal wttrin--favorite-override "Salt Lake City, Utah"))
+ (should (equal wttrin--saved-locations-runtime '(("SLC" . "40.65,-111.87"))))))
+
+(ert-deftest test-wttrin-state-file-normal-set-refreshes-mode-line-on-change ()
+ "Normal: when the restore changes the effective favorite and the mode-line
+mode is on, the mode-line is refreshed so it shows the restored city."
+ (test-wttrin-state-file--with-path-sandbox
+ (let ((wttrin-state-file path)
+ (wttrin--favorite-override "Restored, UT"))
+ (wttrin--state-save))
+ (let ((refreshes 0)
+ (wttrin-favorite-location "Configured, LA")
+ (wttrin-mode-line-mode t))
+ (cl-letf (((symbol-function 'wttrin--mode-line-refresh-now)
+ (lambda () (setq refreshes (1+ refreshes)))))
+ (customize-set-variable 'wttrin-state-file path))
+ (should (= refreshes 1)))))
+
+(ert-deftest test-wttrin-state-file-boundary-set-same-favorite-no-refresh ()
+ "Boundary: a restore that leaves the effective favorite unchanged does not
+refresh the mode-line."
+ (test-wttrin-state-file--with-path-sandbox
+ (let ((wttrin-state-file path)
+ (wttrin--favorite-override "Same, UT"))
+ (wttrin--state-save))
+ (let ((refreshes 0)
+ (wttrin--favorite-override "Same, UT")
+ (wttrin-mode-line-mode t))
+ (cl-letf (((symbol-function 'wttrin--mode-line-refresh-now)
+ (lambda () (setq refreshes (1+ refreshes)))))
+ (customize-set-variable 'wttrin-state-file path))
+ (should (= refreshes 0)))))
+
+(ert-deftest test-wttrin-state-file-boundary-set-mode-line-off-no-refresh ()
+ "Boundary: a favorite change with the mode-line mode off does not refresh."
+ (test-wttrin-state-file--with-path-sandbox
+ (let ((wttrin-state-file path)
+ (wttrin--favorite-override "Restored, UT"))
+ (wttrin--state-save))
+ (let ((refreshes 0)
+ (wttrin-mode-line-mode nil))
+ (cl-letf (((symbol-function 'wttrin--mode-line-refresh-now)
+ (lambda () (setq refreshes (1+ refreshes)))))
+ (customize-set-variable 'wttrin-state-file path))
+ (should (equal wttrin--favorite-override "Restored, UT"))
+ (should (= refreshes 0)))))
+
+(ert-deftest test-wttrin-state-file-boundary-set-absent-file-nil-vars-noop ()
+ "Boundary: customizing to a path with no file and nil vars only sets the
+path — nothing is restored and no file is created."
+ (test-wttrin-state-file--with-path-sandbox
+ (customize-set-variable 'wttrin-state-file path)
+ (should (equal wttrin-state-file path))
+ (should-not (file-exists-p path))
+ (should (null wttrin--favorite-override))))
+
+(ert-deftest test-wttrin-state-file-boundary-set-absent-file-adopts-vars ()
+ "Boundary: customizing to a path with no file while the vars hold values
+adopts them into the new file, the same contract as `wttrin--state-load'."
+ (test-wttrin-state-file--with-path-sandbox
+ (setq wttrin--favorite-override "Adopted, TX")
+ (customize-set-variable 'wttrin-state-file path)
+ (should (file-exists-p path))
+ (should (equal (plist-get (wttrin--state-read) :favorite-override)
+ "Adopted, TX"))))
+
+(ert-deftest test-wttrin-state-file-error-set-corrupt-file-does-not-signal ()
+ "Error: customizing to a path holding a corrupt file sets the path, leaves
+the vars untouched, and does not signal."
+ (test-wttrin-state-file--with-path-sandbox
+ (with-temp-file path (insert "(((( not lisp"))
+ (setq wttrin--favorite-override "Kept, OK")
+ (should-not
+ (condition-case nil
+ (progn (customize-set-variable 'wttrin-state-file path) nil)
+ (error t)))
+ (should (equal wttrin-state-file path))
+ (should (equal wttrin--favorite-override "Kept, OK"))))
+
(provide 'test-wttrin-state-file)
;;; test-wttrin-state-file.el ends here
diff --git a/wttrin.el b/wttrin.el
index ca9d0fd..a524d84 100644
--- a/wttrin.el
+++ b/wttrin.el
@@ -666,6 +666,28 @@ would otherwise drop the entry before it could be saved."
(wttrin--savehist-register)
(add-hook 'savehist-save-hook #'wttrin--savehist-register))
+(defun wttrin--state-file-set (symbol value)
+ "Set SYMBOL (`wttrin-state-file') to VALUE and restore state from it.
+The `:set' handler for the option. The load-time restore below runs against
+whatever path the option holds when wttrin loads, so a path customized
+afterward — the use-package `:config' shape, `(setopt wttrin-state-file ...)'
+after the require — would be written by the location commands but never
+read back on the next start. Re-running `wttrin--state-load' here closes
+that gap for `setopt', `customize-set-variable', and Customize. When the
+restore changes the effective favorite and the mode-line mode is on, the
+mode-line is refreshed so it shows the restored city rather than the
+configured one. At definition time the loader is not yet defined, so the
+`fboundp' guard makes that first call a plain `set-default'; the load-time
+restore covers the initial read."
+ (let ((before (and (fboundp 'wttrin--favorite-location)
+ (wttrin--favorite-location))))
+ (set-default symbol value)
+ (when (fboundp 'wttrin--state-load)
+ (wttrin--state-load)
+ (when (and (not (equal before (wttrin--favorite-location)))
+ (bound-and-true-p wttrin-mode-line-mode))
+ (wttrin--mode-line-refresh-now)))))
+
(defcustom wttrin-state-file (locate-user-emacs-file "wttrin-state.el")
"File persisting wttrin's runtime state across sessions.
Holds the runtime favorite (`wttrin--favorite-override') and the runtime
@@ -675,11 +697,14 @@ geolocation commands. A dedicated file rather than savehist because savehist
rewrites its whole file keeping only the variables registered in the current
process: any Emacs session that saved savehist without wttrin loaded would
silently delete the persisted favorite. Only wttrin writes this file.
-Customize this before wttrin loads (e.g. via use-package `:custom'); the
-state restores at load time, so a later `setq' takes effect only from the
-next restore (`savehist-mode-hook' or restart)."
+The state restores at load time from this path. Set the option with
+`setopt', `customize-set-variable', or Customize (use-package `:custom' or
+`:config' both work): changing it after wttrin has loaded re-reads the state
+from the new path. A plain `setq' after load skips that restore and takes
+effect only from the next one (`savehist-mode-hook' or restart)."
:group 'wttrin
- :type 'file)
+ :type 'file
+ :set #'wttrin--state-file-set)
(defun wttrin--state-save ()
"Write the runtime favorite and directory to `wttrin-state-file'.