From efd3cdce5b3aebfdb3e02460d1ec0434cef85949 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 24 Jun 2026 00:10:51 -0400 Subject: feat: add 'd' key to make the displayed location the default I bound d in the weather buffer to wttrin-make-default, which sets wttrin-favorite-location to the location on screen so it drives the mode-line and future sessions. The footer advertises "[d] to make default". Persistence rides savehist, not the Emacs custom-variable mechanism: wttrin--savehist-register registers wttrin-favorite-location alongside the search history, at load and on savehist-save-hook. Enable savehist-mode and the favorite survives restarts. Promoting a location drops it from the search history, the way wttrin-default-locations entries are kept out of history. The favorite shows in the picker instead: wttrin--completion-candidates prepends it when it's a string and not already a default, so it appears exactly once. The setter only assigns the variable and trims history. It doesn't register with savehist itself, because savehist-additional-variables is unbound until savehist loads, so a direct add-to-list would error for users without savehist. Registration stays on the load and save-hook path. --- README.org | 4 +- tests/test-wttrin--add-buffer-instructions.el | 12 +-- tests/test-wttrin-location-history.el | 8 +- tests/test-wttrin-make-default.el | 130 ++++++++++++++++++++++++++ wttrin.el | 48 ++++++++-- 5 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 tests/test-wttrin-make-default.el diff --git a/README.org b/README.org index 82b36b4..909bd52 100644 --- a/README.org +++ b/README.org @@ -106,7 +106,9 @@ Simply use the keybinding you assigned, or run `M-x wttrin` to display the weath [[assets/location-menu.png]] -Choose one, or for a quick one-time weather check, type a new location and ⏎ . After the weather is displayed, you can press `a` to check another location, `g` to refresh, or `q` to quit. +Choose one, or for a quick one-time weather check, type a new location and ⏎ . After the weather is displayed, you can press `a` to check another location, `g` to refresh, `d` to make the shown location your default, or `q` to quit. + +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. 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)"). diff --git a/tests/test-wttrin--add-buffer-instructions.el b/tests/test-wttrin--add-buffer-instructions.el index 0f1c382..425832d 100644 --- a/tests/test-wttrin--add-buffer-instructions.el +++ b/tests/test-wttrin--add-buffer-instructions.el @@ -28,7 +28,7 @@ "Test adding instructions to empty buffer." (with-temp-buffer (wttrin--add-buffer-instructions) - (should (string= "\n\nPress: [a] for another location [g] to refresh [q] to quit" + (should (string= "\n\nPress: [a] for another location [g] to refresh [d] to make default [q] to quit" (buffer-string))))) (ert-deftest test-wttrin--add-buffer-instructions-normal-with-existing-content-appends-instructions () @@ -36,7 +36,7 @@ (with-temp-buffer (insert "Weather: Sunny\nTemperature: 20°C") (wttrin--add-buffer-instructions) - (should (string= "Weather: Sunny\nTemperature: 20°C\n\nPress: [a] for another location [g] to refresh [q] to quit" + (should (string= "Weather: Sunny\nTemperature: 20°C\n\nPress: [a] for another location [g] to refresh [d] to make default [q] to quit" (buffer-string))))) (ert-deftest test-wttrin--add-buffer-instructions-normal-preserves-point-moves-to-end () @@ -99,7 +99,7 @@ (insert "Weather data here") (goto-char (point-min)) (wttrin--add-buffer-instructions) - (should (string-suffix-p "Press: [a] for another location [g] to refresh [q] to quit" + (should (string-suffix-p "Press: [a] for another location [g] to refresh [d] to make default [q] to quit" (buffer-string))))) (ert-deftest test-wttrin--add-buffer-instructions-boundary-point-in-middle-appends-at-end () @@ -109,7 +109,7 @@ (goto-char (point-min)) (forward-line 1) (wttrin--add-buffer-instructions) - (should (string-suffix-p "Press: [a] for another location [g] to refresh [q] to quit" + (should (string-suffix-p "Press: [a] for another location [g] to refresh [d] to make default [q] to quit" (buffer-string))))) (ert-deftest test-wttrin--add-buffer-instructions-boundary-trailing-newlines-preserves-newlines () @@ -117,7 +117,7 @@ (with-temp-buffer (insert "Weather\n\n\n") (wttrin--add-buffer-instructions) - (should (string= "Weather\n\n\n\n\nPress: [a] for another location [g] to refresh [q] to quit" + (should (string= "Weather\n\n\n\n\nPress: [a] for another location [g] to refresh [d] to make default [q] to quit" (buffer-string))))) (ert-deftest test-wttrin--add-buffer-instructions-boundary-very-large-buffer-appends-at-end () @@ -126,7 +126,7 @@ (insert (make-string 10000 ?x)) (wttrin--add-buffer-instructions) (goto-char (point-max)) - (should (looking-back "Press: \\[a\\] for another location \\[g\\] to refresh \\[q\\] to quit" nil)))) + (should (looking-back "Press: \\[a\\] for another location \\[g\\] to refresh \\[d\\] to make default \\[q\\] to quit" nil)))) ;;; Error Cases diff --git a/tests/test-wttrin-location-history.el b/tests/test-wttrin-location-history.el index d23bdcd..61b495c 100644 --- a/tests/test-wttrin-location-history.el +++ b/tests/test-wttrin-location-history.el @@ -211,11 +211,13 @@ (should (memq 'wttrin--location-history savehist-additional-variables)))) (ert-deftest test-wttrin-location-history-boundary-savehist-register-idempotent () - "Registering when already present does not duplicate the entry." + "Registering an already-present variable does not duplicate it." (require 'savehist) - (let ((savehist-additional-variables '(wttrin--location-history))) + (require 'cl-lib) + (let ((savehist-additional-variables '(wttrin--location-history wttrin-favorite-location))) (wttrin--savehist-register) - (should (equal '(wttrin--location-history) savehist-additional-variables)))) + (should (= 1 (cl-count 'wttrin--location-history savehist-additional-variables))) + (should (= 1 (cl-count 'wttrin-favorite-location savehist-additional-variables))))) (ert-deftest test-wttrin-location-history-integration-savehist-register-on-save-hook () "The registration runs on `savehist-save-hook' so it survives a clobber." diff --git a/tests/test-wttrin-make-default.el b/tests/test-wttrin-make-default.el new file mode 100644 index 0000000..c6f845b --- /dev/null +++ b/tests/test-wttrin-make-default.el @@ -0,0 +1,130 @@ +;;; test-wttrin-make-default.el --- Tests for promote-to-default command -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: + +;; 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. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'wttrin) + +;;; -------------------------------------------------------------------------- +;;; wttrin--set-favorite-location +;;; -------------------------------------------------------------------------- + +;;; Normal Cases + +(ert-deftest test-wttrin--set-favorite-location-normal-sets-variable () + "Normal: sets `wttrin-favorite-location' to the given location." + (let ((wttrin-favorite-location nil) + (savehist-additional-variables nil)) + (wttrin--set-favorite-location "Paris, FR") + (should (equal wttrin-favorite-location "Paris, FR")))) + +(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)) + ;; 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"))))) + +(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--location-history '("Reykjavik" "Oslo, NO"))) + (wttrin--set-favorite-location "Reykjavik") + (should-not (member "Reykjavik" wttrin--location-history)) + (should (equal wttrin--location-history '("Oslo, NO"))))) + +(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--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." + (require 'savehist) + (let ((savehist-additional-variables '(kill-ring))) + (wttrin--savehist-register) + (should (memq 'wttrin-favorite-location savehist-additional-variables)))) + +;;; -------------------------------------------------------------------------- +;;; wttrin-make-default +;;; -------------------------------------------------------------------------- + +;;; Normal Cases + +(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) + (savehist-additional-variables nil)) + (with-temp-buffer + (setq-local wttrin--current-location "Tokyo, JP") + (wttrin-make-default) + (should (equal wttrin-favorite-location "Tokyo, JP"))))) + +;;; 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") + (savehist-additional-variables nil)) + (with-temp-buffer + (setq-local wttrin--current-location nil) + (wttrin-make-default) + (should (equal wttrin-favorite-location "Berkeley, CA"))))) + +;;; -------------------------------------------------------------------------- +;;; favorite in completion candidates +;;; -------------------------------------------------------------------------- + +;;; Normal Cases + +(ert-deftest test-wttrin-make-default-normal-favorite-prepended-to-candidates () + "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")) + (should (equal (wttrin--completion-candidates) + '("Reykjavik" "Honolulu, HI" "Berkeley, CA"))))) + +;;; Boundary Cases + +(ert-deftest test-wttrin-make-default-boundary-favorite-default-not-duplicated () + "Boundary: a favorite that is already a default appears exactly once." + (require 'cl-lib) + (let ((wttrin-default-locations '("Honolulu, HI" "Berkeley, CA")) + (wttrin--location-history nil) + (wttrin-favorite-location "Berkeley, CA")) + (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)) + (should (equal (wttrin--completion-candidates) + '("Honolulu, HI" "Oslo, NO"))))) + +;;; -------------------------------------------------------------------------- +;;; keymap binding +;;; -------------------------------------------------------------------------- + +(ert-deftest test-wttrin-make-default-normal-d-bound-in-mode-map () + "Normal: the weather-buffer keymap binds \"d\" to the command." + (should (eq (lookup-key wttrin-mode-map (kbd "d")) 'wttrin-make-default))) + +(provide 'test-wttrin-make-default) +;;; test-wttrin-make-default.el ends here diff --git a/wttrin.el b/wttrin.el index b1c6562..c4b31bc 100644 --- a/wttrin.el +++ b/wttrin.el @@ -536,11 +536,14 @@ Persisted across sessions via `savehist-mode'.") (defvar savehist-additional-variables) (defun wttrin--savehist-register () - "Ensure `wttrin--location-history' is persisted by savehist. + "Ensure wttrin's persisted variables are saved by savehist. +Registers `wttrin--location-history' and `wttrin-favorite-location' so both +survive across restarts without the Emacs custom-variable mechanism. 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 entry before it could be saved." - (add-to-list 'savehist-additional-variables 'wttrin--location-history)) +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)) (with-eval-after-load 'savehist (wttrin--savehist-register) @@ -563,9 +566,16 @@ entry is promoted to most-recent, and the list is trimmed to (- (length wttrin--location-history) max))))))) (defun wttrin--completion-candidates () - "Return default locations followed by search-history entries. -History already excludes defaults (see `wttrin--add-to-location-history')." - (append wttrin-default-locations wttrin--location-history)) + "Return the favorite, default locations, then search-history entries. +History already excludes defaults (see `wttrin--add-to-location-history'), and +`wttrin--set-favorite-location' drops the favorite from history. The favorite +\(`wttrin-favorite-location', when a string) is prepended unless it is already a +default, so it always appears exactly once." + (let ((candidates (append wttrin-default-locations wttrin--location-history))) + (if (and (stringp wttrin-favorite-location) + (not (member wttrin-favorite-location candidates))) + (cons wttrin-favorite-location candidates) + candidates))) (defun wttrin-remove-location-history (location) "Remove LOCATION from the search history. @@ -602,6 +612,7 @@ Prompts with completion over the current history entries." (let ((map (make-sparse-keymap))) (define-key map (kbd "a") 'wttrin-requery) (define-key map (kbd "g") 'wttrin-requery-force) + (define-key map (kbd "d") 'wttrin-make-default) ;; Note: 'q' is bound to quit-window by special-mode map) "Keymap for wttrin-mode.") @@ -664,6 +675,8 @@ Bracketed key chords use `wttrin-key'; the surrounding prose uses (" for another location " . wttrin-instructions) ("[g]" . wttrin-key) (" to refresh " . wttrin-instructions) + ("[d]" . wttrin-key) + (" to make default " . wttrin-instructions) ("[q]" . wttrin-key) (" to quit" . wttrin-instructions))) (insert (propertize (car segment) 'face (cdr segment))))) @@ -836,6 +849,29 @@ reject inaccurate results." (wttrin-query wttrin--current-location)) (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." + (setq wttrin-favorite-location location) + (setq wttrin--location-history (delete location wttrin--location-history))) + +(defun wttrin-make-default () + "Make the location shown in this buffer the favorite (persisted) default. +Sets `wttrin-favorite-location' to the displayed location so it drives the +mode-line and survives restarts. No-op with a message when the buffer has +no current location." + (interactive) + (if wttrin--current-location + (progn + (wttrin--set-favorite-location wttrin--current-location) + (message "wttrin: %s is now the default location" wttrin--current-location)) + (message "wttrin: no location to make default"))) + ;;; Mode-line weather display (defun wttrin--replace-response-location (response location) -- cgit v1.2.3