diff options
| -rw-r--r-- | debug-wttrin.el | 52 | ||||
| -rw-r--r-- | wttrin-debug.el | 107 | ||||
| -rw-r--r-- | wttrin.el | 229 |
3 files changed, 331 insertions, 57 deletions
diff --git a/debug-wttrin.el b/debug-wttrin.el deleted file mode 100644 index 8b8798b..0000000 --- a/debug-wttrin.el +++ /dev/null @@ -1,52 +0,0 @@ -;;; debug-wttrin.el --- Debug helper for wttrin -*- lexical-binding: t -*- -;; -;; This file provides utilities for debugging wttrin display issues. -;; -;;; Commentary: -;; -;; To enable debug mode: -;; (setq wttrin-debug t) -;; -;; This will save raw weather responses to timestamped files in your -;; temp directory for bug reports. -;; -;; To view raw data with line numbers for development: -;; M-x debug-wttrin-show-raw RET <location> RET -;; -;;; Code: - -(require 'wttrin) - -(defun debug-wttrin-show-raw (location) - "Fetch and display raw wttr.in data for LOCATION with line numbers. -This is useful for debugging header parsing issues." - (interactive "sLocation: ") - (let ((raw-string (wttrin--get-cached-or-fetch location))) - (with-current-buffer (get-buffer-create "*wttrin-debug*") - (erase-buffer) - (insert raw-string) - (goto-char (point-min)) - (let ((line-num 1)) - (while (not (eobp)) - (beginning-of-line) - (insert (format "%2d: " line-num)) - (setq line-num (1+ line-num)) - (forward-line 1))) - (goto-char (point-min)) - (switch-to-buffer (current-buffer))))) - -(defun debug-wttrin-enable () - "Enable wttrin debug mode. -Raw weather data will be saved to timestamped files for bug reports." - (interactive) - (setq wttrin-debug t) - (message "Wttrin debug mode enabled. Raw data will be saved to: %s" temporary-file-directory)) - -(defun debug-wttrin-disable () - "Disable wttrin debug mode." - (interactive) - (setq wttrin-debug nil) - (message "Wttrin debug mode disabled")) - -(provide 'debug-wttrin) -;;; debug-wttrin.el ends here diff --git a/wttrin-debug.el b/wttrin-debug.el new file mode 100644 index 0000000..55546d4 --- /dev/null +++ b/wttrin-debug.el @@ -0,0 +1,107 @@ +;;; wttrin-debug.el --- Debug functions for wttrin.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2025 Craig Jennings +;; Author: Craig Jennings <c@cjennings.net> +;; Keywords: debug weather wttrin + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: + +;; This file contains debug functions for troubleshooting wttrin.el behavior. +;; It is only loaded when `wttrin-debug' is non-nil. +;; +;; Enable with: +;; (setq wttrin-debug t) +;; (require 'wttrin) +;; +;; Available debug functions: +;; - `debug-wttrin-show-raw' - View raw weather data with line numbers +;; - `debug-wttrin-mode-line' - Diagnose mode-line lighter issues +;; - `wttrin--debug-mode-line-info' - Auto-called when wttrin runs (if debug enabled) +;; +;; Interactive commands: +;; - M-x debug-wttrin-enable - Enable debug mode +;; - M-x debug-wttrin-disable - Disable debug mode +;; +;; When debug mode is enabled, raw weather data is automatically saved to +;; timestamped files in `temporary-file-directory' for bug reports. + +;;; Code: + +;; wttrin-debug.el is loaded by wttrin.el, so wttrin is already loaded +;; No need for (require 'wttrin) here + +;;;###autoload +(defun debug-wttrin-show-raw (location) + "Fetch and display raw wttr.in data for LOCATION with line numbers. +This is useful for debugging header parsing issues." + (interactive "sLocation: ") + (let ((raw-string (wttrin--get-cached-or-fetch location))) + (with-current-buffer (get-buffer-create "*wttrin-debug*") + (erase-buffer) + (insert raw-string) + (goto-char (point-min)) + (let ((line-num 1)) + (while (not (eobp)) + (beginning-of-line) + (insert (format "%2d: " line-num)) + (setq line-num (1+ line-num)) + (forward-line 1))) + (goto-char (point-min)) + (switch-to-buffer (current-buffer))))) + +;;;###autoload +(defun debug-wttrin-enable () + "Enable wttrin debug mode. +Raw weather data will be saved to timestamped files for bug reports." + (interactive) + (setq wttrin-debug t) + (message "Wttrin debug mode enabled. Raw data will be saved to: %s" temporary-file-directory)) + +;;;###autoload +(defun debug-wttrin-disable () + "Disable wttrin debug mode." + (interactive) + (setq wttrin-debug nil) + (message "Wttrin debug mode disabled")) + +;;;###autoload +(defun debug-wttrin-mode-line () + "Display detailed mode-line information for the wttrin buffer. +This is useful for diagnosing why the mode-line lighter isn't appearing." + (interactive) + (if-let ((buf (get-buffer "*wttr.in*"))) + (with-current-buffer buf + (let* ((has-custom-modeline (boundp 'cj/modeline-major-mode)) + (formatted-mode (when has-custom-modeline + (format-mode-line mode-name)))) + (with-output-to-temp-buffer "*wttrin-mode-debug*" + (princ (format "=== Wttrin Mode-Line Debug Info ===\n\n")) + (princ (format "Buffer: %s\n" (buffer-name))) + (princ (format "Major mode: %s\n" major-mode)) + (princ (format "mode-name variable: %S\n" mode-name)) + (princ (format "mode-name type: %s\n" (type-of mode-name))) + (princ (format "\nCustom modeline detected: %s\n" has-custom-modeline)) + (when has-custom-modeline + (princ (format "format-mode-line result: %S\n" formatted-mode))) + (princ (format "\nmode-line-format first 5 elements:\n")) + (let ((i 0)) + (dolist (elem mode-line-format) + (when (< i 5) + (princ (format " [%d] %S\n" i elem)) + (setq i (1+ i))))) + (princ (format "\nSpecial-mode parent: %s\n" + (get 'wttrin-mode 'derived-mode-parent))) + (princ (format "Is special-mode active: %s\n" + (derived-mode-p 'special-mode)))))) + (message "No *wttr.in* buffer exists. Run M-x wttrin first."))) + +(defun wttrin--debug-mode-line-info () + "Auto-generate mode-line diagnostic information. +This function is called automatically when wttrin runs if debug mode is enabled. +It creates the *wttrin-mode-debug* buffer with diagnostic information." + (debug-wttrin-mode-line)) + +(provide 'wttrin-debug) +;;; wttrin-debug.el ends here @@ -37,6 +37,9 @@ (require 'url) (require 'xterm-color) ;; https://github.com/atomontage/xterm-color +;; Declare functions from wttrin-debug.el (loaded conditionally) +(declare-function wttrin--debug-mode-line-info "wttrin-debug") + (defgroup wttrin nil "Emacs frontend for the weather web service wttr.in." :prefix "wttrin-" @@ -92,19 +95,70 @@ units (default)." :group 'wttrin :type 'integer) +(defcustom wttrin-mode-line-favorite-location nil + "Favorite location to display weather for in the mode-line. +When nil, mode-line weather display is disabled. +Set to a location string (e.g., \"New Orleans, LA\") to enable. +The weather icon and tooltip will update automatically in the background." + :group 'wttrin + :type '(choice (const :tag "Disabled" nil) + (string :tag "Location"))) + +(defcustom wttrin-mode-line-refresh-interval 900 + "Interval in seconds to refresh mode-line weather data. +Default is 900 seconds (15 minutes)." + :group 'wttrin + :type 'integer) + +(defcustom wttrin-mode-line-emoji-font "Noto Color Emoji" + "Font family to use for mode-line weather emoji. +Common color emoji fonts include: +- \"Noto Color Emoji\" (Linux) +- \"Apple Color Emoji\" (macOS) +- \"Segoe UI Emoji\" (Windows) +- \"Twitter Color Emoji\" +Set to nil to use default font (may render as monochrome)." + :group 'wttrin + :type '(choice (const :tag "Use default font" nil) + (string :tag "Font family name"))) + (defcustom wttrin-debug nil - "If non-nil, save raw weather data to timestamped files for debugging. -Raw data files are saved to `temporary-file-directory' with names like -wttrin-debug-YYYYMMDD-HHMMSS.txt for bug reports." + "Enable debug functions for troubleshooting wttrin behavior. +When non-nil, loads wttrin-debug.el which provides: +- Automatic mode-line diagnostic logging when wttrin runs +- Raw weather data saved to timestamped files in `temporary-file-directory' +- Interactive debug commands for troubleshooting + +Set this to t BEFORE loading wttrin, typically in your init file: + (setq wttrin-debug t) + (require \\='wttrin)" :group 'wttrin :type 'boolean) +;; Load debug functions if enabled +(when wttrin-debug + (require 'wttrin-debug + (expand-file-name "wttrin-debug.el" + (file-name-directory (or load-file-name buffer-file-name))) + t)) + (defvar wttrin--cache (make-hash-table :test 'equal) "Cache for weather data: cache-key -> (timestamp . data).") (defvar wttrin--force-refresh nil "When non-nil, bypass cache on next fetch.") +(defvar wttrin-mode-line-string nil + "Mode-line string showing weather for favorite location.") +;;;###autoload(put 'wttrin-mode-line-string 'risky-local-variable t) +(put 'wttrin-mode-line-string 'risky-local-variable t) + +(defvar wttrin--mode-line-timer nil + "Timer object for mode-line weather refresh.") + +(defvar wttrin--mode-line-tooltip-data nil + "Cached full weather data for tooltip display.") + (defun wttrin-additional-url-params () "Concatenates extra information into the URL." (if wttrin-unit-system @@ -174,7 +228,7 @@ CALLBACK is called with the weather data string when ready, or nil on error." map) "Keymap for wttrin-mode.") -(define-derived-mode wttrin-mode special-mode "⛅" +(define-derived-mode wttrin-mode special-mode "Wttrin" "Major mode for displaying wttr.in weather information. Weather data is displayed in a read-only buffer with the following keybindings: @@ -233,7 +287,14 @@ Returns the path to the saved file." (wttrin-mode) ;; Set location after mode initialization (mode calls kill-all-local-variables) - (setq-local wttrin--current-location location-name)))) + (setq-local wttrin--current-location location-name) + + ;; Auto-generate debug diagnostics if debug mode is enabled + (when (featurep 'wttrin-debug) + (wttrin--debug-mode-line-info)) + + ;; Clear any lingering messages from url-retrieve + (message nil)))) (defun wttrin-query (location-name) "Asynchronously query weather of LOCATION-NAME, display result when ready." @@ -315,6 +376,164 @@ CALLBACK is called with the weather data string when ready, or nil on error." (wttrin-query wttrin--current-location)) (message "No location to refresh"))) +;;; Mode-line weather display + +(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." + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Fetching weather for %s" wttrin-mode-line-favorite-location)) + (when wttrin-mode-line-favorite-location + (let* ((location wttrin-mode-line-favorite-location) + ;; Custom format: location + emoji + temp + conditions + ;; %l=location, %c=weather emoji, %t=temp, %C=conditions + ;; Note: unit system must come BEFORE format in query string + (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)) + (url-request-extra-headers (list wttrin-default-languages)) + (url-user-agent "curl")) + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: URL = %s" url)) + (url-retrieve + url + (lambda (status) + (let ((data nil)) + (condition-case err + (if (plist-get status :error) + (progn + (message "wttrin mode-line: Network error - %s" + (cdr (plist-get status :error))) + (setq data nil)) + (unwind-protect + (progn + ;; Skip HTTP headers + (goto-char (point-min)) + (re-search-forward "\r?\n\r?\n" nil t) + (setq data (string-trim + (decode-coding-string + (buffer-substring-no-properties (point) (point-max)) + 'utf-8))) + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Received data = %S" data))) + (kill-buffer (current-buffer)))) + (error + (message "wttrin mode-line: Error - %s" (error-message-string err)) + (setq data nil))) + (when data + (wttrin--mode-line-update-display data)))))))) + +(defun wttrin--mode-line-update-display (weather-string) + "Update mode-line display with WEATHER-STRING. +Extracts emoji for mode-line, stores full info for tooltip. +WEATHER-STRING format: \"Location: emoji temp conditions\" (e.g., \"Paris: ☀️ +61°F Clear\")." + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Updating display with: %S" weather-string)) + ;; Store full weather info for tooltip + (setq wttrin--mode-line-tooltip-data weather-string) + ;; Extract just the emoji for mode-line display + ;; Format is "Location: emoji +temp conditions" + ;; We want just the emoji (first character after ": ") + (let* ((emoji (if (string-match ":\\s-*\\(.\\)" weather-string) + (match-string 1 weather-string) + "?")) ; Fallback if parsing fails + ;; Force color emoji rendering by setting font family + (emoji-with-font (if wttrin-mode-line-emoji-font + (propertize emoji + 'face (list :family wttrin-mode-line-emoji-font + :height 1.0)) + emoji))) + (setq wttrin-mode-line-string + (propertize (concat " " emoji-with-font) + 'help-echo (lambda (_window _object _pos) + (or wttrin--mode-line-tooltip-data + (format "Weather for %s\nClick to refresh" + wttrin-mode-line-favorite-location))) + 'mouse-face 'mode-line-highlight + 'local-map (let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'wttrin-mode-line-click) + (define-key map [mode-line mouse-3] + 'wttrin-mode-line-force-refresh) + map)))) + (force-mode-line-update t) + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Display updated, mode-line-string = %S, tooltip = %S" + wttrin-mode-line-string wttrin--mode-line-tooltip-data))) + +(defun wttrin-mode-line-click () + "Handle left-click on mode-line weather widget. +Check cache, refresh if needed, then open weather buffer." + (interactive) + (when wttrin-mode-line-favorite-location + (wttrin wttrin-mode-line-favorite-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-mode-line-favorite-location + (let ((wttrin--force-refresh t)) + (wttrin--mode-line-fetch-weather)))) + +(defun wttrin--mode-line-start () + "Start mode-line weather display and refresh timer." + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Starting mode-line display (location=%s, interval=%s)" + wttrin-mode-line-favorite-location + wttrin-mode-line-refresh-interval)) + (when wttrin-mode-line-favorite-location + ;; Delay initial fetch by 3 seconds to allow network to initialize during startup + (run-at-time 3 nil #'wttrin--mode-line-fetch-weather) + ;; Set up refresh timer (starts after the interval from now) + (when wttrin--mode-line-timer + (cancel-timer wttrin--mode-line-timer)) + (setq wttrin--mode-line-timer + (run-at-time wttrin-mode-line-refresh-interval + wttrin-mode-line-refresh-interval + #'wttrin--mode-line-fetch-weather)) + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Initial fetch scheduled in 3 seconds, then every %s seconds" + wttrin-mode-line-refresh-interval)))) + +(defun wttrin--mode-line-stop () + "Stop mode-line weather display and cancel timer." + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Stopping mode-line display")) + (when wttrin--mode-line-timer + (cancel-timer wttrin--mode-line-timer) + (setq wttrin--mode-line-timer nil)) + (setq wttrin-mode-line-string nil) + (setq wttrin--mode-line-tooltip-data nil) + (force-mode-line-update t)) + +;;;###autoload +(define-minor-mode wttrin-mode-line-mode + "Toggle weather display in mode-line. +When enabled, shows weather for `wttrin-mode-line-favorite-location'." + :global t + :lighter (:eval wttrin-mode-line-string) + (if wttrin-mode-line-mode + (progn + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Mode enabled")) + (wttrin--mode-line-start) + ;; Add modeline string to global-mode-string for custom modelines + (if global-mode-string + (add-to-list 'global-mode-string 'wttrin-mode-line-string 'append) + (setq global-mode-string '("" wttrin-mode-line-string))) + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Added to global-mode-string = %S" global-mode-string))) + (when (featurep 'wttrin-debug) + (message "wttrin mode-line: Mode disabled")) + (wttrin--mode-line-stop) + ;; Remove from global-mode-string + (setq global-mode-string + (delq 'wttrin-mode-line-string global-mode-string)))) + ;;;###autoload (defun wttrin (location) "Display weather information for LOCATION. |
