aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-01 13:51:28 -0400
committerCraig Jennings <c@cjennings.net>2026-07-01 13:51:28 -0400
commitf2d3f6c6586ede7f757ca74ea699982562f069a7 (patch)
tree86f292b6ad19c2affd067d6ba13ce3865629a7c0
parentb1aca874b9a2b5282c0cc83bd4fece981a94e19c (diff)
downloademacs-wttrin-f2d3f6c6586ede7f757ca74ea699982562f069a7.tar.gz
emacs-wttrin-f2d3f6c6586ede7f757ca74ea699982562f069a7.zip
fix: keep a d-set favorite from reverting to the init value on restart
wttrin-favorite-location did two jobs: a defcustom the user sets in init, and the savehist-persisted value that `d`/make-default mutated. Setting it in init (setopt) clobbered the `d` choice on every startup. So a favorite promoted with `d` reverted to the init value after a restart, and the mode-line showed the wrong location's weather. wttrin-saved-locations had the same defect. I split the two roles. The defcustoms stay as the init-set base. New savehist-persisted runtime vars, wttrin--favorite-override and wttrin--saved-locations-runtime, hold what `d`, save, rename, remove, and the geolocation commands write. Reads go through resolvers: wttrin--favorite-location returns the override or the configured favorite, and wttrin--saved-locations overlays the runtime directory on the configured one (runtime wins on a name collision). Init and `d` now write different variables, so neither clobbers the other. Removing a saved location that only comes from init can't delete it, since the runtime removal has nothing to touch. The command now says so instead of reporting a removal that didn't happen. There's no automatic migration. A favorite set in init is unaffected, and a favorite set only with `d` before this version is re-set once with `d`. The README documents the behavior.
-rw-r--r--README.org4
-rw-r--r--tests/test-wttrin-favorite-override.el80
-rw-r--r--tests/test-wttrin-make-default.el45
-rw-r--r--tests/test-wttrin-saved-locations.el185
-rw-r--r--tests/test-wttrin-set-location-from-geolocation.el22
-rw-r--r--tests/test-wttrin-use-current-location.el18
-rw-r--r--wttrin.el208
7 files changed, 396 insertions, 166 deletions
diff --git a/README.org b/README.org
index cd419c1..fd01bf0 100644
--- a/README.org
+++ b/README.org
@@ -108,7 +108,7 @@ Simply use the keybinding you assigned, or run `M-x wttrin` to display the weath
Choose one, or for a quick one-time weather check, type a new location and ⏎ . After the weather is displayed, the footer shows two groups of keys. Keys that act on the view: `a` for another location, `g` to refresh, `q` to quit. Keys that act on your saved locations: `s` to save the shown location, `d` to make it your default, `r` to rename a saved location, and `x` to remove one.
-Pressing `d` sets =wttrin-favorite-location= to the location on screen and remembers it across restarts (via savehist), so the mode-line and future sessions follow it. Your default is also offered in the location list the next time you run =M-x wttrin=. Enable =savehist-mode= for the persistence to stick. (On a geolocation-detected buffer, `d` first prompts for a name and saves it — see Naming Locations.)
+Pressing `d` makes the location on screen your default and remembers it across restarts (via savehist), so the mode-line and future sessions follow it. This works alongside a =wttrin-favorite-location= you set in your init: that option is your base default, and a location you promote with `d` overrides it and persists — so a `d` choice is never wiped out by the init value on the next restart. Your default is also offered in the location list the next time you run =M-x wttrin=. Enable =savehist-mode= for the persistence to stick. (On a geolocation-detected buffer, `d` first prompts for a name and saves it — see Naming Locations.)
If you're looking at cached data, a line below the weather art tells you how old it is (e.g., "Last updated: 2:30 PM (5 minutes ago)").
@@ -284,7 +284,7 @@ If you don't want to type your city by hand, wttrin can detect it for you.
*From the picker (weather here, right now):* run =M-x wttrin= and pick the first entry, "Current location (detect)". wttrin looks up your city via IP geolocation and shows its weather. If the guess is wrong (VPN, mobile hotspot), the detected city is right there in the buffer header, so just open the picker again and type the correct city.
-*Make the detected city your default:* in that weather buffer, press =d=. The detected city becomes =wttrin-favorite-location= (what the mode-line tracks). With =savehist-mode= on, the favorite persists across sessions automatically, since wttrin registers it with savehist. No =customize-save-variable= step is needed.
+*Make the detected city your default:* in that weather buffer, press =d=. The detected city becomes your default (what the mode-line tracks), overriding any =wttrin-favorite-location= you set in your init. With =savehist-mode= on, the choice persists across sessions automatically, since wttrin registers it with savehist. No =customize-save-variable= step is needed.
*Always use my current location:* run =M-x wttrin-use-current-location=, or set the variable directly:
diff --git a/tests/test-wttrin-favorite-override.el b/tests/test-wttrin-favorite-override.el
new file mode 100644
index 0000000..0236901
--- /dev/null
+++ b/tests/test-wttrin-favorite-override.el
@@ -0,0 +1,80 @@
+;;; test-wttrin-favorite-override.el --- Tests for the runtime favorite override -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;;; Commentary:
+;; Unit tests for the split between the configured favorite
+;; (`wttrin-favorite-location', a defcustom set in init) and the runtime
+;; favorite (`wttrin--favorite-override', set by `d'/the geolocation commands
+;; and persisted by savehist). The effective favorite resolves via
+;; `wttrin--favorite-location': the override wins over the config, so a favorite
+;; chosen at runtime is not clobbered by an init that also sets the option.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'wttrin)
+
+;;; wttrin--favorite-location (resolver)
+
+(ert-deftest test-wttrin-favorite-override-normal-config-when-no-override ()
+ "Normal: with no override, the effective favorite is the configured one."
+ (let ((wttrin-favorite-location "New Orleans, LA")
+ (wttrin--favorite-override nil))
+ (should (equal "New Orleans, LA" (wttrin--favorite-location)))))
+
+(ert-deftest test-wttrin-favorite-override-normal-override-wins ()
+ "Normal: a runtime override shadows the configured favorite."
+ (let ((wttrin-favorite-location "New Orleans, LA")
+ (wttrin--favorite-override "Hyatt Place Warwick, RI"))
+ (should (equal "Hyatt Place Warwick, RI" (wttrin--favorite-location)))))
+
+(ert-deftest test-wttrin-favorite-override-boundary-both-nil ()
+ "Boundary: no config and no override yields nil (favorite disabled)."
+ (let ((wttrin-favorite-location nil)
+ (wttrin--favorite-override nil))
+ (should (null (wttrin--favorite-location)))))
+
+(ert-deftest test-wttrin-favorite-override-boundary-auto-detect-override ()
+ "Boundary: the override carries the tri-state t (auto-detect)."
+ (let ((wttrin-favorite-location "New Orleans, LA")
+ (wttrin--favorite-override t))
+ (should (eq t (wttrin--favorite-location)))))
+
+(ert-deftest test-wttrin-favorite-override-regression-init-does-not-clobber ()
+ "Regression: setting the config after an override (as an init `setopt' would)
+does not change the effective favorite. This is the bug where a favorite set
+with `d' reverted to the init value on restart."
+ (let ((wttrin--favorite-override "Hyatt Place Warwick, RI")
+ (wttrin-favorite-location nil))
+ ;; Simulate the init running its (setopt wttrin-favorite-location ...).
+ (setq wttrin-favorite-location "New Orleans, LA")
+ (should (equal "Hyatt Place Warwick, RI" (wttrin--favorite-location)))))
+
+;;; wttrin--set-favorite-location writes the override, not the config
+
+(ert-deftest test-wttrin-favorite-override-normal-setter-writes-override ()
+ "Normal: the setter writes the runtime override and leaves the config intact."
+ (let ((wttrin-favorite-location "New Orleans, LA")
+ (wttrin--favorite-override nil)
+ (wttrin--location-history nil))
+ (wttrin--set-favorite-location "Paris, FR")
+ (should (equal "Paris, FR" wttrin--favorite-override))
+ (should (equal "New Orleans, LA" wttrin-favorite-location))
+ (should (equal "Paris, FR" (wttrin--favorite-location)))))
+
+;;; savehist registration targets the runtime vars, not the config defcustoms
+
+(ert-deftest test-wttrin-favorite-override-normal-savehist-registers-override ()
+ "Normal: savehist persists the override, not the config defcustom."
+ (require 'savehist)
+ (let ((savehist-additional-variables '(kill-ring)))
+ (wttrin--savehist-register)
+ (should (memq 'wttrin--favorite-override savehist-additional-variables))
+ (should (memq 'wttrin--saved-locations-runtime savehist-additional-variables))
+ (should-not (memq 'wttrin-favorite-location savehist-additional-variables))
+ (should-not (memq 'wttrin-saved-locations savehist-additional-variables))))
+
+(provide 'test-wttrin-favorite-override)
+;;; test-wttrin-favorite-override.el ends here
diff --git a/tests/test-wttrin-make-default.el b/tests/test-wttrin-make-default.el
index e715e12..e824194 100644
--- a/tests/test-wttrin-make-default.el
+++ b/tests/test-wttrin-make-default.el
@@ -6,7 +6,9 @@
;; Unit tests for wttrin--set-favorite-location and wttrin-make-default,
;; the weather-buffer command (bound to "d") that promotes the displayed
-;; location to the persisted favorite.
+;; location to the persisted favorite. The favorite is written to the runtime
+;; override `wttrin--favorite-override' (not the `wttrin-favorite-location'
+;; defcustom), and read back through `wttrin--favorite-location'.
;;; Code:
@@ -21,26 +23,30 @@
;;; Normal Cases
(ert-deftest test-wttrin--set-favorite-location-normal-sets-variable ()
- "Normal: sets `wttrin-favorite-location' to the given location."
+ "Normal: sets the runtime favorite (leaving the config option untouched)."
(let ((wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
(savehist-additional-variables nil))
(wttrin--set-favorite-location "Paris, FR")
- (should (equal wttrin-favorite-location "Paris, FR"))))
+ (should (equal wttrin--favorite-override "Paris, FR"))
+ (should (equal "Paris, FR" (wttrin--favorite-location)))))
(ert-deftest test-wttrin--set-favorite-location-error-no-savehist-loaded ()
"Error: setting the favorite works even when savehist is not loaded.
The setter must not touch `savehist-additional-variables' directly (it may be
unbound); persistence is left to `wttrin--savehist-register'."
- (let ((wttrin-favorite-location nil))
+ (let ((wttrin-favorite-location nil)
+ (wttrin--favorite-override nil))
;; Simulate savehist absent: the variable is unbound.
(cl-letf (((symbol-function 'wttrin--savehist-register)
(lambda () (error "Should not be called from the setter"))))
(wttrin--set-favorite-location "Oslo, NO")
- (should (equal wttrin-favorite-location "Oslo, NO")))))
+ (should (equal wttrin--favorite-override "Oslo, NO")))))
(ert-deftest test-wttrin--set-favorite-location-normal-drops-from-history ()
"Normal: promoting a location removes it from the search history."
(let ((wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
(wttrin--location-history '("Reykjavik" "Oslo, NO")))
(wttrin--set-favorite-location "Reykjavik")
(should-not (member "Reykjavik" wttrin--location-history))
@@ -49,16 +55,19 @@ unbound); persistence is left to `wttrin--savehist-register'."
(ert-deftest test-wttrin--set-favorite-location-boundary-not-in-history-is-noop ()
"Boundary: promoting a location absent from history leaves history intact."
(let ((wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
(wttrin--location-history '("Oslo, NO")))
(wttrin--set-favorite-location "Berkeley, CA")
(should (equal wttrin--location-history '("Oslo, NO")))))
(ert-deftest test-wttrin-favorite-savehist-register-includes-favorite ()
- "Normal: `wttrin--savehist-register' registers the favorite for persistence."
+ "Normal: `wttrin--savehist-register' registers the runtime override, not the
+`wttrin-favorite-location' defcustom."
(require 'savehist)
(let ((savehist-additional-variables '(kill-ring)))
(wttrin--savehist-register)
- (should (memq 'wttrin-favorite-location savehist-additional-variables))))
+ (should (memq 'wttrin--favorite-override savehist-additional-variables))
+ (should-not (memq 'wttrin-favorite-location savehist-additional-variables))))
;;; --------------------------------------------------------------------------
;;; mode-line refresh when the favorite changes
@@ -70,6 +79,7 @@ unbound); persistence is left to `wttrin--savehist-register'."
"Normal: changing the favorite while the mode-line is active clears the
stale cache and fetches fresh weather for the new location immediately."
(let ((wttrin-favorite-location "Oslo, NO")
+ (wttrin--favorite-override nil)
(wttrin-mode-line-mode t)
(wttrin--mode-line-cache (cons 0.0 "Oslo, NO: sun"))
(fetched nil))
@@ -86,6 +96,7 @@ stale cache and fetches fresh weather for the new location immediately."
(ert-deftest test-wttrin--set-favorite-location-boundary-mode-line-off-no-fetch ()
"Boundary: with the mode-line inactive, changing the favorite does not fetch."
(let ((wttrin-favorite-location "Oslo, NO")
+ (wttrin--favorite-override nil)
(wttrin-mode-line-mode nil)
(fetched nil))
(cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather)
@@ -95,7 +106,8 @@ stale cache and fetches fresh weather for the new location immediately."
(ert-deftest test-wttrin--set-favorite-location-boundary-unchanged-no-fetch ()
"Boundary: re-promoting the current favorite does not refetch the mode-line."
- (let ((wttrin-favorite-location "Paris, FR")
+ (let ((wttrin-favorite-location nil)
+ (wttrin--favorite-override "Paris, FR")
(wttrin-mode-line-mode t)
(fetched nil))
(cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather)
@@ -114,22 +126,26 @@ stale cache and fetches fresh weather for the new location immediately."
(ert-deftest test-wttrin-make-default-normal-sets-favorite-from-current ()
"Normal: promotes the buffer's current location to the favorite."
(let ((wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
+ (wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil)
(savehist-additional-variables nil))
(with-temp-buffer
(setq-local wttrin--current-location "Tokyo, JP")
(wttrin-make-default)
- (should (equal wttrin-favorite-location "Tokyo, JP")))))
+ (should (equal "Tokyo, JP" (wttrin--favorite-location))))))
;;; Boundary Cases
(ert-deftest test-wttrin-make-default-boundary-nil-current-leaves-favorite ()
"Boundary: no current location is a no-op that leaves the favorite intact."
(let ((wttrin-favorite-location "Berkeley, CA")
+ (wttrin--favorite-override nil)
(savehist-additional-variables nil))
(with-temp-buffer
(setq-local wttrin--current-location nil)
(wttrin-make-default)
- (should (equal wttrin-favorite-location "Berkeley, CA")))))
+ (should (equal "Berkeley, CA" (wttrin--favorite-location))))))
;;; --------------------------------------------------------------------------
;;; favorite in completion candidates
@@ -141,7 +157,8 @@ stale cache and fetches fresh weather for the new location immediately."
"Normal: a typed-in favorite is offered in the picker, at the front."
(let ((wttrin-default-locations '("Honolulu, HI" "Berkeley, CA"))
(wttrin--location-history nil)
- (wttrin-favorite-location "Reykjavik"))
+ (wttrin-favorite-location "Reykjavik")
+ (wttrin--favorite-override nil))
(should (equal (wttrin--completion-candidates)
(list wttrin--geolocation-sentinel
"Reykjavik" "Honolulu, HI" "Berkeley, CA")))))
@@ -153,14 +170,16 @@ stale cache and fetches fresh weather for the new location immediately."
(require 'cl-lib)
(let ((wttrin-default-locations '("Honolulu, HI" "Berkeley, CA"))
(wttrin--location-history nil)
- (wttrin-favorite-location "Berkeley, CA"))
+ (wttrin-favorite-location "Berkeley, CA")
+ (wttrin--favorite-override nil))
(should (= 1 (cl-count "Berkeley, CA" (wttrin--completion-candidates) :test #'equal)))))
(ert-deftest test-wttrin-make-default-boundary-nil-favorite-candidates-unchanged ()
"Boundary: nil favorite leaves the candidate list as defaults plus history."
(let ((wttrin-default-locations '("Honolulu, HI"))
(wttrin--location-history '("Oslo, NO"))
- (wttrin-favorite-location nil))
+ (wttrin-favorite-location nil)
+ (wttrin--favorite-override nil))
(should (equal (wttrin--completion-candidates)
(list wttrin--geolocation-sentinel "Honolulu, HI" "Oslo, NO")))))
diff --git a/tests/test-wttrin-saved-locations.el b/tests/test-wttrin-saved-locations.el
index de97df0..1a08aaf 100644
--- a/tests/test-wttrin-saved-locations.el
+++ b/tests/test-wttrin-saved-locations.el
@@ -3,10 +3,20 @@
;; Copyright (C) 2024-2026 Craig Jennings
;;; Commentary:
-;; Unit tests for the named-locations directory (Phase 1): the normalizer
+;; Unit tests for the named-locations directory: the normalizer/union
;; `wttrin--saved-locations', the resolver `wttrin--resolve-location-query',
;; candidate de-duplication/precedence, favorite-as-name resolution, alias cache
;; identity, history suppression of saved names, and savehist registration.
+;;
+;; The directory has two layers: the configured `wttrin-saved-locations'
+;; defcustom (a seed the user sets in init) and the runtime
+;; `wttrin--saved-locations-runtime' (savehist-persisted, mutated by `d',
+;; `wttrin-save-location', rename, and remove). `wttrin--saved-locations'
+;; returns the union, runtime winning on a name collision. Read-only tests set
+;; up the directory via the config layer; mutation tests use the runtime layer,
+;; since the runtime commands cannot delete a config-seeded entry. The favorite
+;; is likewise split: the `wttrin-favorite-location' defcustom plus the runtime
+;; `wttrin--favorite-override', read back through `wttrin--favorite-location'.
;;; Code:
@@ -15,42 +25,61 @@
(require 'wttrin)
(require 'testutil-wttrin)
-;;; wttrin--saved-locations (normalizer)
+;;; wttrin--saved-locations (normalizer/union)
(ert-deftest test-wttrin-saved-locations-normal-pairs-returned ()
"Normal: well-formed pairs are returned as (NAME . QUERY)."
- (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans"))))
+ (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans")))
+ (wttrin--saved-locations-runtime nil))
(should (equal '(("Home" . "1500 Sugar Bowl Dr, New Orleans"))
(wttrin--saved-locations)))))
(ert-deftest test-wttrin-saved-locations-boundary-bare-string-shorthand ()
"Boundary: a bare string S becomes (S . S)."
- (let ((wttrin-saved-locations '("Berkeley, CA")))
+ (let ((wttrin-saved-locations '("Berkeley, CA"))
+ (wttrin--saved-locations-runtime nil))
(should (equal '(("Berkeley, CA" . "Berkeley, CA"))
(wttrin--saved-locations)))))
(ert-deftest test-wttrin-saved-locations-boundary-whitespace-trimmed ()
"Boundary: surrounding whitespace on name and query is trimmed."
- (let ((wttrin-saved-locations '((" Home " . " Paris, FR "))))
+ (let ((wttrin-saved-locations '((" Home " . " Paris, FR ")))
+ (wttrin--saved-locations-runtime nil))
(should (equal '(("Home" . "Paris, FR")) (wttrin--saved-locations)))))
(ert-deftest test-wttrin-saved-locations-error-malformed-skipped ()
"Error: non-cons, non-string, and empty entries are skipped, not fatal."
(let ((wttrin-saved-locations
- (list '("Good" . "Tokyo") 42 '("" . "x") '("y" . "") " " '(a . b))))
+ (list '("Good" . "Tokyo") 42 '("" . "x") '("y" . "") " " '(a . b)))
+ (wttrin--saved-locations-runtime nil))
(should (equal '(("Good" . "Tokyo")) (wttrin--saved-locations)))))
+(ert-deftest test-wttrin-saved-locations-normal-runtime-overlays-config ()
+ "Normal: a runtime entry wins over a config entry with the same name."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin--saved-locations-runtime '(("Home" . "Tokyo, JP"))))
+ (should (equal '(("Home" . "Tokyo, JP")) (wttrin--saved-locations)))))
+
+(ert-deftest test-wttrin-saved-locations-normal-union-runtime-then-config ()
+ "Normal: distinct config and runtime entries both appear, runtime first."
+ (let ((wttrin-saved-locations '(("Work" . "Tokyo, JP")))
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR"))))
+ (should (equal '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP"))
+ (wttrin--saved-locations)))))
+
;;; wttrin--resolve-location-query
(ert-deftest test-wttrin-saved-locations-normal-resolve-name-to-query ()
"Normal: a saved name resolves to its query."
- (let ((wttrin-saved-locations '(("Craig's House" . "1500 Sugar Bowl Dr, New Orleans"))))
+ (let ((wttrin-saved-locations '(("Craig's House" . "1500 Sugar Bowl Dr, New Orleans")))
+ (wttrin--saved-locations-runtime nil))
(should (equal "1500 Sugar Bowl Dr, New Orleans"
(wttrin--resolve-location-query "Craig's House")))))
(ert-deftest test-wttrin-saved-locations-boundary-resolve-passthrough ()
"Boundary: a non-saved selection passes through unchanged."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin--saved-locations-runtime nil))
(should (equal "Tokyo, JP" (wttrin--resolve-location-query "Tokyo, JP")))))
;;; Candidate de-duplication and precedence
@@ -61,7 +90,9 @@
(unwind-protect
(let ((wttrin-geolocation-enabled nil)
(wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin--saved-locations-runtime nil)
(wttrin-favorite-location "Reykjavik")
+ (wttrin--favorite-override nil)
(wttrin-default-locations '("Honolulu, HI"))
(wttrin--location-history '("Tokyo")))
(should (equal '("Home" "Reykjavik" "Honolulu, HI" "Tokyo")
@@ -74,7 +105,9 @@
(unwind-protect
(let ((wttrin-geolocation-enabled nil)
(wttrin-saved-locations '(("Honolulu, HI" . "Honolulu, HI")))
+ (wttrin--saved-locations-runtime nil)
(wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
(wttrin-default-locations '("Honolulu, HI" "Berkeley, CA"))
(wttrin--location-history nil))
(should (equal '("Honolulu, HI" "Berkeley, CA")
@@ -86,14 +119,18 @@
(ert-deftest test-wttrin-saved-locations-normal-favorite-name-resolves-to-query ()
"Normal: a favorite that is a saved name resolves to its query for fetching."
(let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans")))
- (wttrin-favorite-location "Home"))
+ (wttrin--saved-locations-runtime nil)
+ (wttrin-favorite-location "Home")
+ (wttrin--favorite-override nil))
(should (equal "1500 Sugar Bowl Dr, New Orleans"
(wttrin--resolve-favorite-location)))))
(ert-deftest test-wttrin-saved-locations-normal-favorite-display-shows-name ()
"Normal: the favorite display name is the saved name, not its query."
(let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans")))
- (wttrin-favorite-location "Home"))
+ (wttrin--saved-locations-runtime nil)
+ (wttrin-favorite-location "Home")
+ (wttrin--favorite-override nil))
(should (equal "Home" (wttrin--favorite-location-display-name)))))
;;; Alias cache identity
@@ -101,7 +138,8 @@
(ert-deftest test-wttrin-saved-locations-normal-cache-keyed-on-query ()
"Normal: cache identity follows the query, not the display name.
Two names with the same query share a key; the name never leaks into the key."
- (let ((wttrin-saved-locations '(("A" . "Paris, FR") ("B" . "Paris, FR"))))
+ (let ((wttrin-saved-locations '(("A" . "Paris, FR") ("B" . "Paris, FR")))
+ (wttrin--saved-locations-runtime nil))
(should (equal (wttrin--make-cache-key (wttrin--resolve-location-query "A"))
(wttrin--make-cache-key (wttrin--resolve-location-query "B"))))))
@@ -112,6 +150,7 @@ Two names with the same query share a key; the name never leaks into the key."
(testutil-wttrin-setup)
(unwind-protect
(let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin--saved-locations-runtime nil)
(wttrin-default-locations '())
(wttrin--location-history nil))
(wttrin--add-to-location-history "Home")
@@ -121,11 +160,13 @@ Two names with the same query share a key; the name never leaks into the key."
;;; savehist
(ert-deftest test-wttrin-saved-locations-integration-savehist-registers ()
- "Integration: wttrin-saved-locations is registered for savehist persistence."
+ "Integration: the runtime directory is registered for savehist persistence,
+not the `wttrin-saved-locations' defcustom (which the user sets in init)."
(require 'savehist)
(let ((savehist-additional-variables '(kill-ring)))
(wttrin--savehist-register)
- (should (memq 'wttrin-saved-locations savehist-additional-variables))))
+ (should (memq 'wttrin--saved-locations-runtime savehist-additional-variables))
+ (should-not (memq 'wttrin-saved-locations savehist-additional-variables))))
;;; wttrin--coordinates-p
@@ -141,7 +182,8 @@ Two names with the same query share a key; the name never leaks into the key."
(ert-deftest test-wttrin-saved-locations-normal-put-adds-and-updates ()
"Normal: put adds a new entry and updates an existing name without duplicating."
- (let ((wttrin-saved-locations nil))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil))
(wttrin--put-saved-location "Home" "Paris, FR")
(should (equal "Paris, FR" (wttrin--resolve-location-query "Home")))
(wttrin--put-saved-location "Home" "Tokyo, JP")
@@ -150,7 +192,8 @@ Two names with the same query share a key; the name never leaks into the key."
(ert-deftest test-wttrin-saved-locations-error-put-rejects-empty-and-sentinel ()
"Error: put refuses an empty name, empty query, or the sentinel name."
- (let ((wttrin-saved-locations nil))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil))
(should-error (wttrin--put-saved-location "" "Paris") :type 'user-error)
(should-error (wttrin--put-saved-location "Home" "") :type 'user-error)
(should-error (wttrin--put-saved-location wttrin--geolocation-sentinel "x")
@@ -160,25 +203,30 @@ Two names with the same query share a key; the name never leaks into the key."
(ert-deftest test-wttrin-saved-locations-normal-rename ()
"Normal: rename moves the entry and updates the favorite reference."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
- (wttrin-favorite-location "Home"))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR")))
+ (wttrin-favorite-location nil)
+ (wttrin--favorite-override "Home"))
(cl-letf (((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-rename-location "Home" "Casa"))
(should (equal "Paris, FR" (wttrin--resolve-location-query "Casa")))
(should-not (assoc "Home" (wttrin--saved-locations)))
- (should (equal "Casa" wttrin-favorite-location))))
+ (should (equal "Casa" (wttrin--favorite-location)))))
(ert-deftest test-wttrin-saved-locations-error-rename-collision-refused ()
"Error: renaming onto an existing name is refused and changes nothing."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP"))))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP"))))
(should-error (wttrin-rename-location "Home" "Work") :type 'user-error)
(should (equal "Paris, FR" (wttrin--resolve-location-query "Home")))))
(ert-deftest test-wttrin-saved-locations-normal-rename-favorite-refreshes-mode-line ()
"Normal: renaming the favorite refreshes the mode-line so the icon and tooltip
follow the new name immediately instead of at the next scheduled fetch."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
- (wttrin-favorite-location "Home")
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR")))
+ (wttrin-favorite-location nil)
+ (wttrin--favorite-override "Home")
(wttrin--location-history nil)
(wttrin-mode-line-mode t)
(fetched nil))
@@ -188,13 +236,15 @@ follow the new name immediately instead of at the next scheduled fetch."
(lambda () nil))
((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-rename-location "Home" "Casa"))
- (should (equal "Casa" wttrin-favorite-location))
+ (should (equal "Casa" (wttrin--favorite-location)))
(should fetched)))
(ert-deftest test-wttrin-saved-locations-boundary-rename-non-favorite-no-refresh ()
"Boundary: renaming a location that is not the favorite does not refresh."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")))
- (wttrin-favorite-location "Work")
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")))
+ (wttrin-favorite-location nil)
+ (wttrin--favorite-override "Work")
(wttrin--location-history nil)
(wttrin-mode-line-mode t)
(fetched nil))
@@ -210,7 +260,8 @@ follow the new name immediately instead of at the next scheduled fetch."
(ert-deftest test-wttrin-saved-locations-normal-remove-confirmed ()
"Normal: confirming removes the entry."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR"))))
(cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-remove-location "Home"))
@@ -218,17 +269,30 @@ follow the new name immediately instead of at the next scheduled fetch."
(ert-deftest test-wttrin-saved-locations-boundary-remove-declined-keeps ()
"Boundary: declining the confirmation keeps the entry."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR"))))
(cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) nil))
((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-remove-location "Home"))
(should (assoc "Home" (wttrin--saved-locations)))))
+(ert-deftest test-wttrin-saved-locations-boundary-remove-config-entry-remains ()
+ "Boundary: removing a config-seeded entry (not in the runtime layer) leaves it
+in the directory, since the runtime removal cannot delete an init-defined entry."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin--saved-locations-runtime nil))
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-remove-location "Home"))
+ (should (assoc "Home" (wttrin--saved-locations)))))
+
(ert-deftest test-wttrin-saved-locations-normal-remove-favorite-refreshes-mode-line ()
"Normal: removing the favorite refreshes the mode-line so it stops showing the
now-deleted alias's resolved weather and re-fetches against the bare query."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
- (wttrin-favorite-location "Home")
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR")))
+ (wttrin-favorite-location nil)
+ (wttrin--favorite-override "Home")
(wttrin-mode-line-mode t)
(fetched nil))
(cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
@@ -242,8 +306,10 @@ now-deleted alias's resolved weather and re-fetches against the bare query."
(ert-deftest test-wttrin-saved-locations-boundary-remove-non-favorite-no-refresh ()
"Boundary: removing a location that is not the favorite does not refresh."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")))
- (wttrin-favorite-location "Work")
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")))
+ (wttrin-favorite-location nil)
+ (wttrin--favorite-override "Work")
(wttrin-mode-line-mode t)
(fetched nil))
(cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
@@ -260,7 +326,9 @@ now-deleted alias's resolved weather and re-fetches against the bare query."
(ert-deftest test-wttrin-saved-locations-normal-d-names-and-promotes ()
"Normal: d on a coordinate buffer names it, saves it, and promotes the name."
(let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil)
(wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
(wttrin-mode-line-mode nil))
(with-temp-buffer
(setq-local wttrin--current-location "41.37,-71.83")
@@ -270,12 +338,14 @@ now-deleted alias's resolved weather and re-fetches against the bare query."
((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-make-default)))
(should (equal "41.37,-71.83" (wttrin--resolve-location-query "Home")))
- (should (equal "Home" wttrin-favorite-location))))
+ (should (equal "Home" (wttrin--favorite-location)))))
(ert-deftest test-wttrin-saved-locations-boundary-d-empty-keeps-coordinates ()
"Boundary: an empty name at the d prompt keeps the coordinates, saves no entry."
(let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil)
(wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
(wttrin-mode-line-mode nil))
(with-temp-buffer
(setq-local wttrin--current-location "41.37,-71.83")
@@ -284,12 +354,14 @@ now-deleted alias's resolved weather and re-fetches against the bare query."
((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-make-default)))
(should (null (wttrin--saved-locations)))
- (should (equal "41.37,-71.83" wttrin-favorite-location))))
+ (should (equal "41.37,-71.83" (wttrin--favorite-location)))))
(ert-deftest test-wttrin-saved-locations-boundary-d-named-buffer-no-prompt ()
"Boundary: d on a named buffer promotes the display name without prompting."
(let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil)
(wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
(wttrin-mode-line-mode nil)
(prompted nil))
(with-temp-buffer
@@ -300,7 +372,7 @@ now-deleted alias's resolved weather and re-fetches against the bare query."
((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-make-default)))
(should-not prompted)
- (should (equal "Craig's House" wttrin-favorite-location))
+ (should (equal "Craig's House" (wttrin--favorite-location)))
(should (equal "1500 Sugar Bowl Dr"
(wttrin--resolve-location-query "Craig's House")))))
@@ -308,14 +380,16 @@ now-deleted alias's resolved weather and re-fetches against the bare query."
"Normal: making a typed location the default also saves it to the directory,
so it persists as a named entry rather than only as the favorite string."
(let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil)
(wttrin-favorite-location nil)
+ (wttrin--favorite-override nil)
(wttrin-mode-line-mode nil))
(with-temp-buffer
(setq-local wttrin--current-location "Reykjavik")
(setq-local wttrin--current-display "Reykjavik")
(cl-letf (((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-make-default)))
- (should (equal "Reykjavik" wttrin-favorite-location))
+ (should (equal "Reykjavik" (wttrin--favorite-location)))
(should (assoc "Reykjavik" (wttrin--saved-locations)))
(should (equal "Reykjavik" (wttrin--resolve-location-query "Reykjavik")))))
@@ -323,7 +397,8 @@ so it persists as a named entry rather than only as the favorite string."
(ert-deftest test-wttrin-saved-locations-normal-save-location-interactive ()
"Normal: the interactive save command reads the buffer query and a name."
- (let ((wttrin-saved-locations nil))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil))
(with-temp-buffer
(setq-local wttrin--current-location "Paris, FR")
(setq-local wttrin--current-display "Paris, FR")
@@ -334,7 +409,8 @@ so it persists as a named entry rather than only as the favorite string."
(ert-deftest test-wttrin-saved-locations-normal-rename-interactive ()
"Normal: the interactive rename command prompts for the entry and new name."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR"))))
(cl-letf (((symbol-function 'completing-read) (lambda (&rest _) "Home"))
((symbol-function 'read-string) (lambda (&rest _) "Casa"))
((symbol-function 'message) (lambda (&rest _) nil)))
@@ -343,7 +419,8 @@ so it persists as a named entry rather than only as the favorite string."
(ert-deftest test-wttrin-saved-locations-normal-remove-interactive ()
"Normal: the interactive remove command prompts and confirms."
- (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime '(("Home" . "Paris, FR"))))
(cl-letf (((symbol-function 'completing-read) (lambda (&rest _) "Home"))
((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
((symbol-function 'message) (lambda (&rest _) nil)))
@@ -352,7 +429,8 @@ so it persists as a named entry rather than only as the favorite string."
(ert-deftest test-wttrin-saved-locations-boundary-save-empty-name-cancels ()
"Boundary: an empty name at the save prompt cancels without saving."
- (let ((wttrin-saved-locations nil))
+ (let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil))
(with-temp-buffer
(setq-local wttrin--current-location "Paris, FR")
(cl-letf (((symbol-function 'read-string) (lambda (&rest _) " "))
@@ -368,6 +446,7 @@ so it persists as a named entry rather than only as the favorite string."
(unwind-protect
(let ((wttrin-default-locations '())
(wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil)
(wttrin--location-history nil))
(wttrin--add-to-location-history "41.37,-71.83")
(should (null wttrin--location-history)))
@@ -379,40 +458,12 @@ so it persists as a named entry rather than only as the favorite string."
"Normal: saving a location drops its query from history, so the place lives in
the directory only and does not also appear as a separate history candidate."
(let ((wttrin-saved-locations nil)
+ (wttrin--saved-locations-runtime nil)
(wttrin--location-history '("New Orleans" "Paris")))
(cl-letf (((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-save-location "Home" "New Orleans"))
(should-not (member "New Orleans" wttrin--location-history))
(should (member "Paris" wttrin--location-history))))
-(ert-deftest test-wttrin-saved-locations-normal-d-alias-drops-query-from-history ()
- "Normal: making an aliased location the default drops its underlying query from
-history, so the place does not also linger as a separate history candidate."
- (let ((wttrin-saved-locations nil)
- (wttrin-favorite-location nil)
- (wttrin-mode-line-mode nil)
- (wttrin--location-history '("New Orleans" "Paris")))
- (with-temp-buffer
- (setq-local wttrin--current-location "New Orleans")
- (setq-local wttrin--current-display "Home")
- (cl-letf (((symbol-function 'message) (lambda (&rest _) nil)))
- (wttrin-make-default)))
- (should (equal "Home" wttrin-favorite-location))
- (should (assoc "Home" (wttrin--saved-locations)))
- (should-not (member "New Orleans" wttrin--location-history))
- (should (member "Paris" wttrin--location-history))))
-
-(ert-deftest test-wttrin-saved-locations-normal-remove-forgets-history ()
- "Normal: removing a saved location drops both its name and its query from
-history, so a removed place does not resurface as a history candidate."
- (let ((wttrin-saved-locations '(("Home" . "New Orleans")))
- (wttrin--location-history '("Home" "New Orleans" "Paris")))
- (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
- ((symbol-function 'message) (lambda (&rest _) nil)))
- (wttrin-remove-location "Home"))
- (should-not (member "Home" wttrin--location-history))
- (should-not (member "New Orleans" wttrin--location-history))
- (should (member "Paris" wttrin--location-history))))
-
(provide 'test-wttrin-saved-locations)
;;; test-wttrin-saved-locations.el ends here
diff --git a/tests/test-wttrin-set-location-from-geolocation.el b/tests/test-wttrin-set-location-from-geolocation.el
index f16b2ae..10e5cc0 100644
--- a/tests/test-wttrin-set-location-from-geolocation.el
+++ b/tests/test-wttrin-set-location-from-geolocation.el
@@ -20,16 +20,24 @@
(defvar test-wttrin-set-location-from-geolocation--saved-favorite nil
"Snapshot of `wttrin-favorite-location' restored in teardown.")
+(defvar test-wttrin-set-location-from-geolocation--saved-override nil
+ "Snapshot of `wttrin--favorite-override' restored in teardown.")
+
(defun test-wttrin-set-location-from-geolocation-setup ()
- "Snapshot `wttrin-favorite-location' and clear it for the test."
+ "Snapshot the configured favorite and runtime override, clearing both."
(setq test-wttrin-set-location-from-geolocation--saved-favorite
wttrin-favorite-location)
- (setq wttrin-favorite-location nil))
+ (setq test-wttrin-set-location-from-geolocation--saved-override
+ wttrin--favorite-override)
+ (setq wttrin-favorite-location nil)
+ (setq wttrin--favorite-override nil))
(defun test-wttrin-set-location-from-geolocation-teardown ()
- "Restore `wttrin-favorite-location' to its pre-test value."
+ "Restore the configured favorite and runtime override to pre-test values."
(setq wttrin-favorite-location
- test-wttrin-set-location-from-geolocation--saved-favorite))
+ test-wttrin-set-location-from-geolocation--saved-favorite)
+ (setq wttrin--favorite-override
+ test-wttrin-set-location-from-geolocation--saved-override))
;;; Helpers
@@ -51,7 +59,7 @@
(progn
(test-wttrin-set-location--with-detected "Berkeley, California" t
(wttrin-set-location-from-geolocation))
- (should (string= "Berkeley, California" wttrin-favorite-location)))
+ (should (string= "Berkeley, California" (wttrin--favorite-location))))
(test-wttrin-set-location-from-geolocation-teardown)))
(ert-deftest test-wttrin-set-location-from-geolocation-normal-decline-leaves-variable-unchanged ()
@@ -84,7 +92,7 @@ favorite immediately instead of at the next scheduled fetch."
((symbol-function 'wttrin--mode-line-set-placeholder)
(lambda () nil)))
(wttrin-set-location-from-geolocation))
- (should (string= "Berkeley, California" wttrin-favorite-location))
+ (should (string= "Berkeley, California" (wttrin--favorite-location)))
(should fetched))
(test-wttrin-set-location-from-geolocation-teardown)))
@@ -97,7 +105,7 @@ favorite immediately instead of at the next scheduled fetch."
(progn
(test-wttrin-set-location--with-detected "München, Bayern" t
(wttrin-set-location-from-geolocation))
- (should (string= "München, Bayern" wttrin-favorite-location)))
+ (should (string= "München, Bayern" (wttrin--favorite-location))))
(test-wttrin-set-location-from-geolocation-teardown)))
;;; Error Cases
diff --git a/tests/test-wttrin-use-current-location.el b/tests/test-wttrin-use-current-location.el
index 4b61657..d7d0ea6 100644
--- a/tests/test-wttrin-use-current-location.el
+++ b/tests/test-wttrin-use-current-location.el
@@ -18,14 +18,20 @@
(defvar test-wttrin-use-current-location--saved nil
"Snapshot of `wttrin-favorite-location' restored in teardown.")
+(defvar test-wttrin-use-current-location--saved-override nil
+ "Snapshot of `wttrin--favorite-override' restored in teardown.")
+
(defun test-wttrin-use-current-location-setup ()
- "Snapshot `wttrin-favorite-location' and clear it."
+ "Snapshot the configured favorite and the runtime override, clearing both."
(setq test-wttrin-use-current-location--saved wttrin-favorite-location)
- (setq wttrin-favorite-location nil))
+ (setq test-wttrin-use-current-location--saved-override wttrin--favorite-override)
+ (setq wttrin-favorite-location nil)
+ (setq wttrin--favorite-override nil))
(defun test-wttrin-use-current-location-teardown ()
- "Restore `wttrin-favorite-location'."
- (setq wttrin-favorite-location test-wttrin-use-current-location--saved))
+ "Restore the configured favorite and the runtime override."
+ (setq wttrin-favorite-location test-wttrin-use-current-location--saved)
+ (setq wttrin--favorite-override test-wttrin-use-current-location--saved-override))
;;; Normal Cases
@@ -37,7 +43,7 @@
(cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin-use-current-location))
- (should (eq t wttrin-favorite-location)))
+ (should (eq t (wttrin--favorite-location))))
(test-wttrin-use-current-location-teardown)))
(ert-deftest test-wttrin-use-current-location-normal-decline-leaves-unchanged ()
@@ -69,7 +75,7 @@ location immediately rather than at the next scheduled fetch."
((symbol-function 'wttrin--mode-line-set-placeholder)
(lambda () nil)))
(wttrin-use-current-location))
- (should (eq t wttrin-favorite-location))
+ (should (eq t (wttrin--favorite-location)))
(should fetched))
(test-wttrin-use-current-location-teardown)))
diff --git a/wttrin.el b/wttrin.el
index ef83b45..8bfa74e 100644
--- a/wttrin.el
+++ b/wttrin.el
@@ -244,6 +244,21 @@ A bare string S used anywhere a location is expected is shorthand for
:type '(alist :key-type (string :tag "Name")
:value-type (string :tag "Query")))
+(defvar wttrin--favorite-override nil
+ "Runtime favorite set by `d'/`wttrin-make-default' and the geolocation commands.
+Persisted across sessions via savehist. When non-nil it shadows the configured
+`wttrin-favorite-location', so a favorite chosen at runtime survives a restart
+even when the user also sets `wttrin-favorite-location' in their init. Carries
+the same tri-state shape as that option: nil (no override), a string, or t
+\(auto-detect).")
+
+(defvar wttrin--saved-locations-runtime nil
+ "Saved-location entries added at runtime via `d' or `wttrin-save-location'.
+Persisted across sessions via savehist. Overlaid on the configured
+`wttrin-saved-locations' by `wttrin--saved-locations', runtime winning on a name
+collision, so directory edits survive a restart without being clobbered by an
+init that also sets `wttrin-saved-locations'. An alist of (NAME . QUERY).")
+
(defvar wttrin--resolved-favorite-location nil
"Cached geolocation result for `wttrin-favorite-location' = t.
Holds the resolved \"City, Region\" string so subsequent reads
@@ -254,9 +269,18 @@ do not re-fetch. Reset implicitly when the Emacs session ends.")
Prevents duplicate concurrent lookups when several consumers ask
during the resolution window.")
+(defun wttrin--favorite-location ()
+ "Return the effective favorite: the runtime override, else the configured one.
+`wttrin--favorite-override' (set at runtime by `d' and the geolocation commands,
+persisted by savehist) wins over the `wttrin-favorite-location' defcustom, so a
+runtime choice is not clobbered by an init that also sets the option. Carries
+the option's tri-state shape: nil, a string, or t."
+ (or wttrin--favorite-override wttrin-favorite-location))
+
(defun wttrin--resolve-favorite-location ()
"Return the favorite location's query string, or nil if unavailable.
-Resolves `wttrin-favorite-location' across the three modes:
+Resolves the effective favorite (`wttrin--favorite-location') across the three
+modes:
- nil -> nil (disabled)
- a string -> its saved-locations query when the string is a saved name,
otherwise the string as-is (the query for a plain location)
@@ -264,15 +288,16 @@ Resolves `wttrin-favorite-location' across the three modes:
and no lookup is in flight, kicks off an async detect
and returns nil for this call. The next call after the
lookup completes returns the resolved string."
- (cond
- ((null wttrin-favorite-location) nil)
- ((stringp wttrin-favorite-location)
- (wttrin--resolve-location-query wttrin-favorite-location))
- ((eq wttrin-favorite-location t)
- (or wttrin--resolved-favorite-location
- (progn
- (wttrin--start-favorite-location-detect)
- nil)))))
+ (let ((fav (wttrin--favorite-location)))
+ (cond
+ ((null fav) nil)
+ ((stringp fav)
+ (wttrin--resolve-location-query fav))
+ ((eq fav t)
+ (or wttrin--resolved-favorite-location
+ (progn
+ (wttrin--start-favorite-location-detect)
+ nil))))))
(defun wttrin--start-favorite-location-detect ()
"Kick off an async geolocation lookup if one is not already pending.
@@ -296,12 +321,14 @@ call retries."
"Return a human-readable name for the favorite location.
For a string favorite this is the string itself (a saved-location name shows as
its name, not its resolved query). For t it is the resolved geolocation place,
-or \"current location\" while a lookup is pending. Nil when disabled."
- (cond
- ((stringp wttrin-favorite-location) wttrin-favorite-location)
- ((eq wttrin-favorite-location t)
- (or wttrin--resolved-favorite-location "current location"))
- (t nil)))
+or \"current location\" while a lookup is pending. Nil when disabled.
+Reads the effective favorite (`wttrin--favorite-location')."
+ (let ((fav (wttrin--favorite-location)))
+ (cond
+ ((stringp fav) fav)
+ ((eq fav t)
+ (or wttrin--resolved-favorite-location "current location"))
+ (t nil))))
(defcustom wttrin-mode-line-refresh-interval 3600
"Interval in seconds to refresh mode-line weather data.
@@ -599,16 +626,20 @@ Persisted across sessions via `savehist-mode'.")
(defvar savehist-additional-variables)
(defun wttrin--savehist-register ()
- "Ensure wttrin's persisted variables are saved by savehist.
-Registers `wttrin--location-history', `wttrin-favorite-location', and
-`wttrin-saved-locations' so they survive across restarts without the Emacs
-custom-variable mechanism.
+ "Ensure wttrin's persisted runtime state is saved by savehist.
+Registers `wttrin--location-history', `wttrin--favorite-override', and
+`wttrin--saved-locations-runtime' so they survive across restarts without the
+Emacs custom-variable mechanism. Deliberately registers the runtime override
+and runtime directory rather than the `wttrin-favorite-location' and
+`wttrin-saved-locations' defcustoms: those are configuration the user sets in
+init, and persisting them would fight the init on every restart (the value the
+user last chose with `d' would be clobbered by the init `setopt').
Run both at load and on `savehist-save-hook', so the registration survives a
user `setq' of `savehist-additional-variables' (a common config pattern) that
would otherwise drop the entries before they could be saved."
(add-to-list 'savehist-additional-variables 'wttrin--location-history)
- (add-to-list 'savehist-additional-variables 'wttrin-favorite-location)
- (add-to-list 'savehist-additional-variables 'wttrin-saved-locations))
+ (add-to-list 'savehist-additional-variables 'wttrin--favorite-override)
+ (add-to-list 'savehist-additional-variables 'wttrin--saved-locations-runtime))
(with-eval-after-load 'savehist
(wttrin--savehist-register)
@@ -650,8 +681,8 @@ other, never both."
(setq wttrin--location-history
(delete location wttrin--location-history)))))
-(defun wttrin--saved-locations ()
- "Return `wttrin-saved-locations' as a clean list of (NAME . QUERY) pairs.
+(defun wttrin--normalize-location-entries (entries)
+ "Return ENTRIES as a clean list of (NAME . QUERY) pairs.
Skips malformed entries — non-cons, a non-string name or query, or an empty
name or query — and trims surrounding whitespace, so stale or hand-edited
config never errors. A bare string S is read as (S . S)."
@@ -667,7 +698,24 @@ config never errors. A bare string S is read as (S . S)."
(let ((s (string-trim entry)))
(and (> (length s) 0) (cons s s))))
(t nil)))
- wttrin-saved-locations)))
+ entries)))
+
+(defun wttrin--saved-locations ()
+ "Return the effective saved-locations directory as clean (NAME . QUERY) pairs.
+The runtime layer `wttrin--saved-locations-runtime' (set by `d' and
+`wttrin-save-location', persisted by savehist) is overlaid on the configured
+`wttrin-saved-locations', runtime winning on a name collision. Both layers are
+normalized (malformed entries skipped, whitespace trimmed, a bare string S read
+as (S . S)); runtime entries are listed first, then config entries whose names
+the runtime does not already define."
+ (let* ((runtime (wttrin--normalize-location-entries wttrin--saved-locations-runtime))
+ (runtime-names (mapcar #'car runtime))
+ (config (wttrin--normalize-location-entries wttrin-saved-locations)))
+ (append runtime
+ (delq nil
+ (mapcar (lambda (entry)
+ (unless (member (car entry) runtime-names) entry))
+ config)))))
(defun wttrin--resolve-location-query (selection)
"Return the query string for a picker SELECTION.
@@ -683,31 +731,38 @@ Used to keep a raw geolocation fix out of history and to decide when the
(and (stringp string)
(string-match-p "\\`[ ]*-?[0-9.]+[ ]*,[ ]*-?[0-9.]+[ ]*\\'" string)))
-(defun wttrin--saved-locations-without (name)
- "Return `wttrin-saved-locations' with any entry named NAME removed."
+(defun wttrin--saved-locations-runtime-without (name)
+ "Return `wttrin--saved-locations-runtime' with any entry named NAME removed."
(delq nil
(mapcar (lambda (entry)
(unless (and (consp entry) (equal (car entry) name)) entry))
- wttrin-saved-locations)))
+ wttrin--saved-locations-runtime)))
(defun wttrin--put-saved-location (name query)
- "Add or update NAME -> QUERY in `wttrin-saved-locations'; return the saved name.
-Trims NAME and QUERY. Signals a `user-error' for an empty name or query, or a
-name equal to the geolocation sentinel. An existing name has its query updated."
+ "Add or update NAME -> QUERY in the runtime directory; return the saved name.
+Writes `wttrin--saved-locations-runtime' (savehist-persisted), never the
+`wttrin-saved-locations' defcustom, so a runtime save is not clobbered by an
+init that also sets the option. Trims NAME and QUERY. Signals a `user-error'
+for an empty name or query, or a name equal to the geolocation sentinel. An
+existing runtime name has its query updated."
(let ((name (string-trim (or name "")))
(query (string-trim (or query ""))))
(when (string= name "") (user-error "Location name cannot be empty"))
(when (string= query "") (user-error "Location query cannot be empty"))
(when (string= name wttrin--geolocation-sentinel)
(user-error "That name is reserved for the geolocation entry"))
- (setq wttrin-saved-locations
- (append (wttrin--saved-locations-without name)
+ (setq wttrin--saved-locations-runtime
+ (append (wttrin--saved-locations-runtime-without name)
(list (cons name query))))
name))
(defun wttrin--remove-saved-location (name)
- "Remove the saved location named NAME from `wttrin-saved-locations'."
- (setq wttrin-saved-locations (wttrin--saved-locations-without name)))
+ "Remove the saved location named NAME from the runtime directory.
+Only the runtime layer `wttrin--saved-locations-runtime' is mutated. An entry
+from the `wttrin-saved-locations' defcustom cannot be deleted here (it is
+re-read from the user's init) and remains in the effective directory."
+ (setq wttrin--saved-locations-runtime
+ (wttrin--saved-locations-runtime-without name)))
(defvar-local wttrin--current-location nil
"Query for the weather shown in this buffer (the fetch/cache identity).")
@@ -745,8 +800,8 @@ history (the explicit alias wins over a same-named default or history string),
so each place appears exactly once. The geolocation sentinel is prepended when
geolocation is enabled."
(let* ((saved (mapcar #'car (wttrin--saved-locations)))
- (favorite (and (stringp wttrin-favorite-location)
- (list wttrin-favorite-location)))
+ (favorite (let ((fav (wttrin--favorite-location)))
+ (and (stringp fav) (list fav))))
(deduped (delete-dups
(append saved favorite
(copy-sequence wttrin-default-locations)
@@ -865,7 +920,7 @@ the favorite is updated to NEW."
(let ((query (cdr entry)))
(wttrin--remove-saved-location old)
(wttrin--put-saved-location new query)
- (when (equal wttrin-favorite-location old)
+ (when (equal (wttrin--favorite-location) old)
(wttrin--set-favorite-location new))
(message "Renamed %s to %s" old new))))))
@@ -880,16 +935,22 @@ When NAME is the favorite, it is left as a literal query with a warning."
((not (assoc name (wttrin--saved-locations)))
(user-error "No saved location named %s" name))
((yes-or-no-p (format "Remove saved location \"%s\"? " name))
- (let ((query (cdr (assoc name (wttrin--saved-locations)))))
+ (let ((query (cdr (assoc name (wttrin--saved-locations))))
+ (was-favorite (equal (wttrin--favorite-location) name)))
(wttrin--remove-saved-location name)
- (wttrin--drop-from-location-history name query))
- (if (equal wttrin-favorite-location name)
- (progn
- (when (bound-and-true-p wttrin-mode-line-mode)
- (wttrin--mode-line-refresh-now))
- (message "Removed %s; it was your favorite and is now a literal query until you set a new one"
- name))
- (message "Removed %s" name)))
+ (wttrin--drop-from-location-history name query)
+ (when (and was-favorite (bound-and-true-p wttrin-mode-line-mode))
+ (wttrin--mode-line-refresh-now))
+ (cond
+ ;; Still resolvable means the name comes from the user's init, which the
+ ;; runtime removal can't touch, so the entry stays in the directory.
+ ((assoc name (wttrin--saved-locations))
+ (message "Removed the runtime entry %s; it is still defined in your init (wttrin-saved-locations) and remains in the directory"
+ name))
+ (was-favorite
+ (message "Removed %s; it was your favorite and is now a literal query until you set a new one"
+ name))
+ (t (message "Removed %s" name)))))
(t (message "Cancelled"))))
(defun wttrin--requery-location (new-location)
@@ -1346,12 +1407,14 @@ This creates headroom to avoid frequent cleanups."
(defun wttrin-set-location-from-geolocation ()
"Detect your location via IP geolocation and set it as the favorite.
Uses the provider named by `wttrin-geolocation-provider' to fetch
-\"City, Region\", asks for confirmation, and on yes assigns the
-result to `wttrin-favorite-location'.
+\"City, Region\", asks for confirmation, and on yes makes the result
+the runtime favorite (`wttrin--favorite-override', which shadows the
+`wttrin-favorite-location' option).
With `savehist-mode' on, the favorite persists across sessions
-automatically (wttrin registers it with savehist); no
-`customize-save-variable' step is needed.
+automatically (wttrin registers the override with savehist); no
+`customize-save-variable' step is needed, and it is not overwritten by
+an init that also sets `wttrin-favorite-location'.
IP-based geolocation can be wrong behind a VPN or a mobile hotspot.
The confirmation prompt shows the detected location so you can
@@ -1373,7 +1436,7 @@ the detected city as your default."
((yes-or-no-p (format "Detected location: %s. Set as favorite? "
location))
(wttrin--set-favorite-location location)
- (message "Set wttrin-favorite-location to: %s%s"
+ (message "Set favorite location to: %s%s"
location
(if (bound-and-true-p savehist-mode)
" (persisted via savehist)."
@@ -1389,13 +1452,14 @@ the detected city as your default."
;;;###autoload
(defun wttrin-use-current-location ()
"Make your current location the persistent favorite (always auto-detect).
-Sets `wttrin-favorite-location' to t after confirmation, so the mode-line
-and buffer track wherever you are via geolocation rather than a fixed city.
-This is the labeled way to choose auto-detect without typing the bare symbol
-t into your init.
-
-With `savehist-mode' on, the choice persists across sessions automatically.
-Does nothing when `wttrin-geolocation-enabled' is nil."
+After confirmation, sets the runtime favorite (`wttrin--favorite-override') to
+t, so the mode-line and buffer track wherever you are via geolocation rather
+than a fixed city. This is the labeled way to choose auto-detect without typing
+the bare symbol t into your init.
+
+With `savehist-mode' on, the choice persists across sessions automatically, and
+is not overwritten by an init that also sets `wttrin-favorite-location'. Does
+nothing when `wttrin-geolocation-enabled' is nil."
(interactive)
(cond
((not wttrin-geolocation-enabled)
@@ -1421,15 +1485,17 @@ Does nothing when `wttrin-geolocation-enabled' is nil."
(message "No location to refresh")))
(defun wttrin--set-favorite-location (location)
- "Set `wttrin-favorite-location' to LOCATION and drop it from search history.
-LOCATION becomes a permanent default, so it no longer needs a history entry,
-mirroring how `wttrin-default-locations' entries are kept out of history.
-Persistence is handled by `wttrin--savehist-register', which registers the
-variable when savehist loads and again on `savehist-save-hook', so the value
-survives restarts without the Emacs custom-variable mechanism, and setting it
-here works whether or not savehist is loaded."
- (let ((changed (not (equal location wttrin-favorite-location))))
- (setq wttrin-favorite-location location)
+ "Set the runtime favorite to LOCATION and drop it from search history.
+Writes `wttrin--favorite-override', not the `wttrin-favorite-location'
+defcustom, so the choice survives a restart even when the user also sets the
+option in their init (the override shadows the config; see
+`wttrin--favorite-location'). LOCATION becomes a permanent default, so it no
+longer needs a history entry, mirroring how `wttrin-default-locations' entries
+are kept out of history. Persistence is handled by `wttrin--savehist-register',
+which registers the override when savehist loads and again on
+`savehist-save-hook', so setting it works whether or not savehist is loaded."
+ (let ((changed (not (equal location (wttrin--favorite-location)))))
+ (setq wttrin--favorite-override location)
(setq wttrin--location-history (delete location wttrin--location-history))
(when (and changed (bound-and-true-p wttrin-mode-line-mode))
(wttrin--mode-line-refresh-now))))
@@ -1528,7 +1594,7 @@ On failure with no cache, shows error placeholder.
When `wttrin-favorite-location' is t and geolocation has not yet
resolved, this call is a no-op; the next tick after resolution
proceeds normally."
- (wttrin--debug-log "mode-line-fetch: Starting fetch for %s" wttrin-favorite-location)
+ (wttrin--debug-log "mode-line-fetch: Starting fetch for %s" (wttrin--favorite-location))
(let ((location (wttrin--resolve-favorite-location)))
(if (not location)
(wttrin--debug-log "mode-line-fetch: No favorite location available, skipping")
@@ -1691,9 +1757,9 @@ mode-line state."
(defun wttrin--mode-line-start ()
"Start mode-line weather display and refresh timer."
(wttrin--debug-log "wttrin mode-line: Starting mode-line display (location=%s, interval=%s)"
- wttrin-favorite-location
+ (wttrin--favorite-location)
wttrin-mode-line-refresh-interval)
- (when wttrin-favorite-location
+ (when (wttrin--favorite-location)
;; Trigger geolocation resolution in the background if needed; the
;; placeholder + scheduled fetch will pick up the resolved string
;; on the next tick.