aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-23 23:04:46 -0400
committerCraig Jennings <c@cjennings.net>2026-06-23 23:04:46 -0400
commit4d46eafaa087f5570ece9d2e5f5d2ba6bc0d824e (patch)
tree19c24dc27a1f72e8b05a98b43a429c49f917fc4a
parenteaabae3b01b8bdc05a5892492c06a8131c195a45 (diff)
downloademacs-wttrin-4d46eafaa087f5570ece9d2e5f5d2ba6bc0d824e.tar.gz
emacs-wttrin-4d46eafaa087f5570ece9d2e5f5d2ba6bc0d824e.zip
feat: expose themeable faces for mode-line and buffer text
I added four customizable faces so themes and customize-face can restyle the text wttrin draws itself: wttrin-mode-line-stale (the dimmed stale mode-line emoji), wttrin-staleness-header (the "Last updated:" line), wttrin-instructions (the footer prose), and wttrin-key (the [a]/[g]/[q] chords). The package exposed no faces before and hardcoded one color. The stale-emoji dimming used a literal "gray60". Now it inherits a face, so the color tracks the theme. I changed make-emoji-icon's second argument from a color string to a face symbol applied via :inherit. wttrin-key inherits bold rather than help-key-binding, which is Emacs 28+ while the package supports 24.4. The weather ASCII art stays colored by xterm-color's ANSI faces. Only the package's own text is newly faced.
-rw-r--r--README.org17
-rw-r--r--tests/test-wttrin--add-buffer-instructions.el18
-rw-r--r--tests/test-wttrin--format-staleness-header.el12
-rw-r--r--tests/test-wttrin--mode-line-helpers.el30
-rw-r--r--tests/test-wttrin--mode-line-update-display.el10
-rw-r--r--tests/test-wttrin-faces.el26
-rw-r--r--wttrin.el61
7 files changed, 142 insertions, 32 deletions
diff --git a/README.org b/README.org
index 658c4ed..82b36b4 100644
--- a/README.org
+++ b/README.org
@@ -293,6 +293,23 @@ The default lookup provider is =ipapi.co=. Two alternatives ship with the packag
*Note:* IP-based geolocation can be wrong when you are behind a VPN or using a mobile hotspot. The confirmation prompt lets you reject an inaccurate result. If you prefer, set =wttrin-favorite-location= directly to any city string that wttr.in understands.
+*** Theming the Faces
+The text wttrin draws itself uses named faces, so themes and =M-x customize-face= can restyle it. (The weather art itself is colored by the ANSI codes wttr.in returns, not by these faces.)
+
+| Face | Styles | Default |
+|---------------------------+------------------------------------------------------+-------------------|
+| =wttrin-mode-line-stale= | the mode-line emoji when its data has gone stale | inherits =shadow= |
+| =wttrin-staleness-header= | the "Last updated: ..." line in the weather buffer | inherits =shadow= |
+| =wttrin-instructions= | the key-hint footer prose in the weather buffer | inherits =shadow= |
+| =wttrin-key= | the bracketed key chords ([a] [g] [q]) in the footer | inherits =bold= |
+
+Restyle them in your init file like any other face:
+
+#+begin_src emacs-lisp
+ (set-face-attribute 'wttrin-key nil :foreground "deep sky blue" :weight 'bold)
+ (set-face-attribute 'wttrin-staleness-header nil :slant 'italic)
+#+end_src
+
** Debugging and Troubleshooting
If something isn't working, debug mode logs every fetch, every display update, and every error.
diff --git a/tests/test-wttrin--add-buffer-instructions.el b/tests/test-wttrin--add-buffer-instructions.el
index 4949c45..0f1c382 100644
--- a/tests/test-wttrin--add-buffer-instructions.el
+++ b/tests/test-wttrin--add-buffer-instructions.el
@@ -73,6 +73,24 @@
(setq count (1+ count))))
(should (= 2 count))))))
+(ert-deftest test-wttrin--add-buffer-instructions-normal-key-chords-carry-key-face ()
+ "Bracketed key chords are styled with `wttrin-key'."
+ (with-temp-buffer
+ (wttrin--add-buffer-instructions)
+ (goto-char (point-min))
+ (search-forward "[a]")
+ ;; point is just after the closing bracket; the bracket char is part
+ ;; of the "[a]" segment
+ (should (eq (get-text-property (1- (point)) 'face) 'wttrin-key))))
+
+(ert-deftest test-wttrin--add-buffer-instructions-normal-prose-carries-instructions-face ()
+ "The footer prose is styled with `wttrin-instructions'."
+ (with-temp-buffer
+ (wttrin--add-buffer-instructions)
+ (goto-char (point-min))
+ (search-forward "Press:")
+ (should (eq (get-text-property (1- (point)) 'face) 'wttrin-instructions))))
+
;;; Boundary Cases
(ert-deftest test-wttrin--add-buffer-instructions-boundary-point-at-beginning-appends-at-end ()
diff --git a/tests/test-wttrin--format-staleness-header.el b/tests/test-wttrin--format-staleness-header.el
index 5658be0..7a39b9a 100644
--- a/tests/test-wttrin--format-staleness-header.el
+++ b/tests/test-wttrin--format-staleness-header.el
@@ -50,6 +50,18 @@
(should (string-match-p "just now" header)))))
(test-wttrin--format-staleness-header-teardown)))
+(ert-deftest test-wttrin--format-staleness-header-normal-carries-face ()
+ "The returned header string is styled with `wttrin-staleness-header'."
+ (test-wttrin--format-staleness-header-setup)
+ (unwind-protect
+ (let ((now 1000000.0))
+ (cl-letf (((symbol-function 'float-time) (lambda () now)))
+ (testutil-wttrin-add-to-cache "Paris" "weather data" 300)
+ (let ((header (wttrin--format-staleness-header "Paris")))
+ (should (eq (get-text-property 0 'face header)
+ 'wttrin-staleness-header)))))
+ (test-wttrin--format-staleness-header-teardown)))
+
;;; Boundary Cases
(ert-deftest test-wttrin--format-staleness-header-boundary-no-cache-returns-nil ()
diff --git a/tests/test-wttrin--mode-line-helpers.el b/tests/test-wttrin--mode-line-helpers.el
index 408711b..21ac167 100644
--- a/tests/test-wttrin--mode-line-helpers.el
+++ b/tests/test-wttrin--mode-line-helpers.el
@@ -46,40 +46,40 @@
(should (equal (plist-get face :family) "Noto Color Emoji"))
(should (equal (plist-get face :height) 1.0))))))
-(ert-deftest test-wttrin--make-emoji-icon-normal-with-foreground ()
- "Foreground color should be applied when specified."
+(ert-deftest test-wttrin--make-emoji-icon-normal-with-face ()
+ "A face should be applied via :inherit when specified."
(let ((wttrin-mode-line-emoji-font nil))
- (let ((result (wttrin--make-emoji-icon "☀" "gray60")))
+ (let ((result (wttrin--make-emoji-icon "☀" 'wttrin-mode-line-stale)))
(let ((face (get-text-property 0 'face result)))
- (should (equal (plist-get face :foreground) "gray60"))))))
+ (should (eq (plist-get face :inherit) 'wttrin-mode-line-stale))))))
-(ert-deftest test-wttrin--make-emoji-icon-normal-with-font-and-foreground ()
- "Both font and foreground should be applied together."
+(ert-deftest test-wttrin--make-emoji-icon-normal-with-font-and-face ()
+ "Both font and face should be applied together."
(let ((wttrin-mode-line-emoji-font "Noto Color Emoji"))
- (let ((result (wttrin--make-emoji-icon "⏳" "gray60")))
+ (let ((result (wttrin--make-emoji-icon "⏳" 'wttrin-mode-line-stale)))
(let ((face (get-text-property 0 'face result)))
(should (equal (plist-get face :family) "Noto Color Emoji"))
- (should (equal (plist-get face :foreground) "gray60"))))))
+ (should (eq (plist-get face :inherit) 'wttrin-mode-line-stale))))))
;;; Boundary Cases
-(ert-deftest test-wttrin--make-emoji-icon-boundary-nil-foreground-no-color ()
- "Nil foreground should not add any :foreground property when no font."
+(ert-deftest test-wttrin--make-emoji-icon-boundary-nil-face-no-font ()
+ "Nil face with no font should return a plain string."
(let ((wttrin-mode-line-emoji-font nil))
(let ((result (wttrin--make-emoji-icon "☀" nil)))
- ;; Without font or foreground, should be plain string
+ ;; Without font or face, should be plain string
(should (equal result "☀")))))
-(ert-deftest test-wttrin--make-emoji-icon-boundary-nil-foreground-with-font ()
- "With font set and nil foreground, the face plist must omit :foreground entirely.
-A literal `:foreground nil' entry triggers \"Invalid face attribute\" warnings on
+(ert-deftest test-wttrin--make-emoji-icon-boundary-nil-face-with-font ()
+ "With font set and nil face, the face plist must omit :inherit entirely.
+A literal `:inherit nil' entry triggers \"Invalid face attribute\" warnings on
every redisplay. `plist-member' (not `plist-get') is required: `plist-get' can't
distinguish a missing key from a present key bound to nil."
(let ((wttrin-mode-line-emoji-font "Noto Color Emoji"))
(let* ((result (wttrin--make-emoji-icon "☀" nil))
(face (get-text-property 0 'face result)))
(should (equal (plist-get face :family) "Noto Color Emoji"))
- (should-not (plist-member face :foreground)))))
+ (should-not (plist-member face :inherit)))))
;;; --------------------------------------------------------------------------
;;; wttrin--set-mode-line-string
diff --git a/tests/test-wttrin--mode-line-update-display.el b/tests/test-wttrin--mode-line-update-display.el
index 0635b5f..721517e 100644
--- a/tests/test-wttrin--mode-line-update-display.el
+++ b/tests/test-wttrin--mode-line-update-display.el
@@ -170,7 +170,7 @@
(test-wttrin--mode-line-update-display-teardown)))
(ert-deftest test-wttrin--mode-line-update-display-stale-emoji-dimmed ()
- "Stale data dims the emoji with gray foreground."
+ "Stale data dims the emoji via the `wttrin-mode-line-stale' face."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
(let ((wttrin-mode-line-refresh-interval 900)
@@ -178,13 +178,13 @@
(cl-letf (((symbol-function 'float-time) (lambda () 3000.0)))
(setq wttrin--mode-line-cache (cons 1000.0 "Paris: X +61°F Clear"))
(wttrin--mode-line-update-display)
- ;; The emoji character should have a gray face
+ ;; The emoji character should inherit the stale face
(let* ((str wttrin-mode-line-string)
;; Find the emoji position (after the space)
(emoji-pos 1)
(face (get-text-property emoji-pos 'face str)))
(should face)
- (should (equal (plist-get face :foreground) "gray60")))))
+ (should (eq (plist-get face :inherit) 'wttrin-mode-line-stale)))))
(test-wttrin--mode-line-update-display-teardown)))
;;; Boundary Cases
@@ -223,7 +223,7 @@ trigger an emoji re-render so dimming matches the tooltip's staleness state."
(wttrin--mode-line-update-display)
;; Emoji should NOT be dimmed
(let ((face (get-text-property 1 'face wttrin-mode-line-string)))
- (should-not (and face (equal (plist-get face :foreground) "gray60")))))
+ (should-not (and face (eq (plist-get face :inherit) 'wttrin-mode-line-stale)))))
;; Time passes: data is now stale (age=2001, threshold=1800)
;; Invoke the tooltip (simulating a hover) — this should trigger a re-render
@@ -233,7 +233,7 @@ trigger an emoji re-render so dimming matches the tooltip's staleness state."
;; After hover detected staleness transition, emoji should now be dimmed
(let ((face (get-text-property 1 'face wttrin-mode-line-string)))
(should face)
- (should (equal (plist-get face :foreground) "gray60")))))
+ (should (eq (plist-get face :inherit) 'wttrin-mode-line-stale)))))
(test-wttrin--mode-line-update-display-teardown)))
;;; --------------------------------------------------------------------------
diff --git a/tests/test-wttrin-faces.el b/tests/test-wttrin-faces.el
new file mode 100644
index 0000000..db30002
--- /dev/null
+++ b/tests/test-wttrin-faces.el
@@ -0,0 +1,26 @@
+;;; test-wttrin-faces.el --- Tests for wttrin themeable faces -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests verifying the package's customizable faces are defined so
+;; themes and `customize-face' can target them.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin-faces-normal-all-defined ()
+ "Normal: every package face is defined after loading wttrin."
+ (dolist (face '(wttrin-mode-line-stale
+ wttrin-staleness-header
+ wttrin-instructions
+ wttrin-key))
+ (should (facep face))))
+
+(provide 'test-wttrin-faces)
+;;; test-wttrin-faces.el ends here
diff --git a/wttrin.el b/wttrin.el
index 150e934..b1c6562 100644
--- a/wttrin.el
+++ b/wttrin.el
@@ -57,6 +57,32 @@
:prefix "wttrin-"
:group 'comm)
+(defface wttrin-mode-line-stale
+ '((t :inherit shadow))
+ "Face for the mode-line weather emoji when its data is stale.
+Applied when a scheduled refresh has failed and the cached reading is
+older than twice `wttrin-mode-line-refresh-interval'. A color emoji
+font may ignore the foreground, in which case the dimming is only
+visible on monochrome glyphs."
+ :group 'wttrin)
+
+(defface wttrin-staleness-header
+ '((t :inherit shadow))
+ "Face for the \"Last updated: ...\" line in the weather buffer."
+ :group 'wttrin)
+
+(defface wttrin-instructions
+ '((t :inherit shadow))
+ "Face for the key-hint footer prose in the weather buffer."
+ :group 'wttrin)
+
+(defface wttrin-key
+ '((t :inherit bold))
+ "Face for the bracketed key chords in the weather buffer footer.
+`help-key-binding' would be the natural parent, but it only exists in
+Emacs 28+, and wttrin supports 24.4, so the default inherits `bold'."
+ :group 'wttrin)
+
(defcustom wttrin-font-name "Liberation Mono"
"Preferred monospaced font name for weather display."
:group 'wttrin
@@ -628,9 +654,19 @@ Returns processed string ready for display."
(buffer-string))))
(defun wttrin--add-buffer-instructions ()
- "Add user instructions at bottom of current buffer."
+ "Add the key-hint footer at the bottom of the current buffer.
+Bracketed key chords use `wttrin-key'; the surrounding prose uses
+`wttrin-instructions'."
(goto-char (point-max))
- (insert "\n\nPress: [a] for another location [g] to refresh [q] to quit"))
+ (insert "\n\n")
+ (dolist (segment '(("Press: " . wttrin-instructions)
+ ("[a]" . wttrin-key)
+ (" for another location " . wttrin-instructions)
+ ("[g]" . wttrin-key)
+ (" to refresh " . wttrin-instructions)
+ ("[q]" . wttrin-key)
+ (" to quit" . wttrin-instructions)))
+ (insert (propertize (car segment) 'face (cdr segment)))))
(defun wttrin--format-staleness-header (location)
"Return a staleness header string for LOCATION, or nil if no cache entry.
@@ -643,7 +679,8 @@ Looks up the cache timestamp for LOCATION and formats a line like
(age (- (float-time) timestamp))
(time-str (format-time-string "%l:%M %p" (seconds-to-time timestamp)))
(age-str (wttrin--format-age age)))
- (format "Last updated: %s (%s)" (string-trim time-str) age-str)))))
+ (propertize (format "Last updated: %s (%s)" (string-trim time-str) age-str)
+ 'face 'wttrin-staleness-header)))))
(defun wttrin--display-weather (location-name raw-string &optional error-msg)
"Display weather data RAW-STRING for LOCATION-NAME in weather buffer.
@@ -809,19 +846,19 @@ user's original casing so tooltips display what the user expects."
(concat location (substring response (match-beginning 0)))
response))
-(defun wttrin--make-emoji-icon (emoji &optional foreground)
- "Create EMOJI string with optional font face and FOREGROUND color.
-Uses `wttrin-mode-line-emoji-font' when configured.
-Omits `:foreground' from the face plist when FOREGROUND is nil — a literal
-`:foreground nil' entry triggers \"Invalid face attribute\" warnings on every
+(defun wttrin--make-emoji-icon (emoji &optional face)
+ "Create EMOJI string, optionally styled with FACE and the emoji font.
+Uses `wttrin-mode-line-emoji-font' when configured. FACE, when non-nil,
+is applied via `:inherit'. Omitting it avoids a literal `:inherit nil'
+entry, which triggers \"Invalid face attribute\" warnings on every
redisplay."
(if wttrin-mode-line-emoji-font
(propertize emoji
'face `(:family ,wttrin-mode-line-emoji-font
:height 1.0
- ,@(when foreground (list :foreground foreground))))
- (if foreground
- (propertize emoji 'face (list :foreground foreground))
+ ,@(when face (list :inherit face))))
+ (if face
+ (propertize emoji 'face (list :inherit face))
emoji)))
(defun wttrin--set-mode-line-string (icon tooltip)
@@ -947,7 +984,7 @@ shows staleness info in tooltip."
emoji stale-p)
(setq wttrin--mode-line-rendered-stale stale-p)
(setq wttrin-mode-line-string
- (propertize (concat " " (wttrin--make-emoji-icon emoji (when stale-p "gray60")))
+ (propertize (concat " " (wttrin--make-emoji-icon emoji (when stale-p 'wttrin-mode-line-stale)))
'help-echo #'wttrin--mode-line-tooltip
'mouse-face 'mode-line-highlight
'local-map wttrin--mode-line-map)))))