diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-05 10:06:20 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-05 10:06:20 -0500 |
| commit | 541586e3ff0606afcf0581210a62c95f5ee65c71 (patch) | |
| tree | d67d1f08323879e72a10b42f945edff9004675a8 /wttrin.el | |
| parent | ec00dbe1c03527ec46a0faa20545a7acd382da84 (diff) | |
| download | emacs-wttrin-541586e3ff0606afcf0581210a62c95f5ee65c71.tar.gz emacs-wttrin-541586e3ff0606afcf0581210a62c95f5ee65c71.zip | |
feat: support t for auto-detect via geolocation in wttrin-favorite-location
Completes the three-mode configuration the favorite-location feature was always meant to have:
- nil — disabled (default; unchanged)
- a string — explicit location (unchanged)
- t — auto-detect via IP geolocation (NEW)
When the user sets `wttrin-favorite-location` to t, wttrin runs the geolocation lookup once on first use and caches the result for the session. Subsequent reads return the cached string. The lookup happens in the background via the existing `wttrin-geolocation-detect`, so Emacs startup is never blocked.
I added two private state vars (`wttrin--resolved-favorite-location`, `wttrin--favorite-location-pending`) and a resolver `wttrin--resolve-favorite-location` that maps the three modes onto a returned string or nil. When t is set and the cache is empty, the resolver kicks off the lookup and returns nil for that call — the next consumer tick after the callback completes gets the cached string. The pending flag prevents duplicate concurrent lookups when several consumers ask during the resolution window.
Five consumer call sites now go through the resolver instead of reading `wttrin-favorite-location` directly: `wttrin--mode-line-fetch-weather`, `wttrin-mode-line-click`, `wttrin-mode-line-force-refresh`, `wttrin--buffer-cache-refresh`, and `wttrin--mode-line-start`. Two display sites (the placeholder and error tooltips) use a new `wttrin--favorite-location-display-name` helper that returns "current location" while a t-mode lookup is pending, instead of showing the literal `t` to the user.
Tests cover the resolver across all three modes, including the pending state, the duplicate-suppression behavior, and detection-failure retry. Existing consumer tests stay green because the resolver returns the bound string unchanged when the variable is a string. One care: the test file requires wttrin-geolocation up front so cl-letf mocks of `wttrin-geolocation-detect` aren't undone by the resolver's lazy require — without that, the first run hit ipapi.co for real.
README documents the new mode under "Setting the Favorite Location from IP Geolocation".
Diffstat (limited to 'wttrin.el')
| -rw-r--r-- | wttrin.el | 137 |
1 files changed, 105 insertions, 32 deletions
@@ -144,14 +144,76 @@ of entries, providing a reasonable buffer before the next cleanup.") (defcustom wttrin-favorite-location nil "Favorite location to display weather for. -When nil, favorite location features are disabled. -Set to a location string (e.g., \"New Orleans, LA\") to enable mode-line -weather display and other location-based features. -The weather icon and tooltip will update automatically in the background." + +Three modes: +- nil Favorite-location features are disabled (default). +- a string Use the string as the location, e.g. \"Berkeley, CA\". +- t Auto-detect via IP geolocation. wttrin runs the lookup + once on first use and caches the result for the session. + To pick a specific provider, customize + `wttrin-geolocation-provider'. + +When set, the weather icon and tooltip update automatically in the +background. IP-based auto-detection can be inaccurate behind a VPN +or a mobile hotspot — use a string if you need accuracy." :group 'wttrin :type '(choice (const :tag "Disabled" nil) + (const :tag "Auto-detect via geolocation" t) (string :tag "Location"))) +(defvar wttrin--resolved-favorite-location nil + "Cached geolocation result for `wttrin-favorite-location' = t. +Holds the resolved \"City, Region\" string so subsequent reads +do not re-fetch. Reset implicitly when the Emacs session ends.") + +(defvar wttrin--favorite-location-pending nil + "Non-nil while a geolocation lookup for the favorite is in flight. +Prevents duplicate concurrent lookups when several consumers ask +during the resolution window.") + +(defun wttrin--resolve-favorite-location () + "Return the favorite location as a string, or nil if unavailable. +Resolves `wttrin-favorite-location' across the three modes: +- nil -> nil (disabled) +- a string -> the string as-is +- t -> the cached geolocation result. When the cache is empty + 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-favorite-location) + ((eq wttrin-favorite-location 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. +On success the resolved string is stored in +`wttrin--resolved-favorite-location'. Failures (network error, parse +error) leave the cache empty and clear the pending flag, so the next +call retries." + (unless wttrin--favorite-location-pending + (setq wttrin--favorite-location-pending t) + (require 'wttrin-geolocation) + (wttrin-geolocation-detect + (lambda (location) + (setq wttrin--favorite-location-pending nil) + (when location + (setq wttrin--resolved-favorite-location location) + (wttrin--debug-log + "Resolved favorite-location via geolocation: %s" location)))))) + +(defun wttrin--favorite-location-display-name () + "Return a human-readable name for the favorite location. +Returns the resolved string when available; otherwise returns +\"current location\" if auto-detect is configured but pending, +or nil if the favorite is disabled." + (or (wttrin--resolve-favorite-location) + (when (eq wttrin-favorite-location t) "current location"))) + (defcustom wttrin-mode-line-refresh-interval 3600 "Interval in seconds to refresh mode-line weather data. Default is 3600 seconds (1 hour). The wttr.in service updates its @@ -675,29 +737,33 @@ e.g., \"Paris: ☀️ +61°F Clear\"." "Update placeholder to show fetch error state. Keeps the hourglass icon but updates tooltip to explain the failure and indicate when retry will occur." - (let ((retry-minutes (ceiling (/ wttrin-mode-line-refresh-interval 60.0)))) + (let ((retry-minutes (ceiling (/ wttrin-mode-line-refresh-interval 60.0))) + (label (or (wttrin--favorite-location-display-name) "favorite"))) (wttrin--set-mode-line-string (wttrin--make-emoji-icon "⏳") (format "Weather fetch failed for %s — will retry in %d minutes" - wttrin-favorite-location retry-minutes)))) + label retry-minutes)))) (defun wttrin--mode-line-fetch-weather () "Fetch weather for favorite location and update mode-line display. Uses wttr.in custom format for concise weather with emoji. On success, writes to `wttrin--mode-line-cache' and updates display. On failure with existing cache, shows stale data. -On failure with no cache, shows error placeholder." +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) - (if (not wttrin-favorite-location) - (wttrin--debug-log "mode-line-fetch: No favorite location set, skipping") - (let* ((location wttrin-favorite-location) - ;; wttr.in format codes: %l=location %c=emoji %t=temp %C=conditions - (format-params (if wttrin-unit-system - (concat "?" wttrin-unit-system "&format=%l:+%c+%t+%C") - "?format=%l:+%c+%t+%C")) - (url (concat "https://wttr.in/" - (url-hexify-string location) - format-params))) + (let ((location (wttrin--resolve-favorite-location))) + (if (not location) + (wttrin--debug-log "mode-line-fetch: No favorite location available, skipping") + (let* (;; wttr.in format codes: %l=location %c=emoji %t=temp %C=conditions + (format-params (if wttrin-unit-system + (concat "?" wttrin-unit-system "&format=%l:+%c+%t+%C") + "?format=%l:+%c+%t+%C")) + (url (concat "https://wttr.in/" + (url-hexify-string location) + format-params))) (wttrin--debug-log "mode-line-fetch: URL = %s" url) (wttrin--fetch-url url @@ -718,7 +784,7 @@ On failure with no cache, shows error placeholder." ;; Have stale cache — update display to show staleness (wttrin--mode-line-update-display) ;; No cache at all — show error placeholder - (wttrin--mode-line-update-placeholder-error)))))))) + (wttrin--mode-line-update-placeholder-error))))))))) (defun wttrin--mode-line-extract-emoji (weather-string) "Extract the emoji character from WEATHER-STRING. @@ -783,14 +849,15 @@ shows staleness info in tooltip." "Handle left-click on mode-line weather widget. Check cache, refresh if needed, then open weather buffer." (interactive) - (when wttrin-favorite-location - (wttrin wttrin-favorite-location))) + (let ((location (wttrin--resolve-favorite-location))) + (when location + (wttrin location)))) (defun wttrin-mode-line-force-refresh () "Handle right-click on mode-line weather widget. Force-refresh cache and update tooltip without opening buffer." (interactive) - (when wttrin-favorite-location + (when (wttrin--resolve-favorite-location) (let ((wttrin--force-refresh t)) (wttrin--mode-line-fetch-weather)))) @@ -798,7 +865,8 @@ Force-refresh cache and update tooltip without opening buffer." "Set a placeholder icon in the mode-line while waiting for weather data." (wttrin--set-mode-line-string (wttrin--make-emoji-icon "⏳") - (format "Fetching weather for %s..." wttrin-favorite-location))) + (format "Fetching weather for %s..." + (or (wttrin--favorite-location-display-name) "favorite")))) (defvar wttrin--buffer-refresh-timer nil "Timer object for proactive buffer cache refresh.") @@ -807,16 +875,17 @@ Force-refresh cache and update tooltip without opening buffer." "Proactively refresh the buffer cache for `wttrin-favorite-location'. Fetches fresh weather data and updates the buffer cache entry without displaying anything. This keeps buffer data fresh for when the user -opens the weather buffer." - (when wttrin-favorite-location - (let* ((location wttrin-favorite-location) - (cache-key (wttrin--make-cache-key location))) - (wttrin-fetch-raw-string - location - (lambda (fresh-data &optional _error-msg) - (when fresh-data - (wttrin--cleanup-cache-if-needed) - (puthash cache-key (cons (float-time) fresh-data) wttrin--cache))))))) +opens the weather buffer. When the favorite is set to t and +geolocation has not yet resolved, this call is a no-op." + (let ((location (wttrin--resolve-favorite-location))) + (when location + (let ((cache-key (wttrin--make-cache-key location))) + (wttrin-fetch-raw-string + location + (lambda (fresh-data &optional _error-msg) + (when fresh-data + (wttrin--cleanup-cache-if-needed) + (puthash cache-key (cons (float-time) fresh-data) wttrin--cache)))))))) (defun wttrin--mode-line-start () "Start mode-line weather display and refresh timer." @@ -824,6 +893,10 @@ opens the weather buffer." wttrin-favorite-location wttrin-mode-line-refresh-interval) (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. + (wttrin--resolve-favorite-location) (wttrin--mode-line-set-placeholder) ;; Delay first fetch — network/daemon may not be ready at startup (run-at-time wttrin-mode-line-startup-delay nil #'wttrin--mode-line-fetch-weather) |
