aboutsummaryrefslogtreecommitdiff
path: root/wttrin-geolocation.el
diff options
context:
space:
mode:
Diffstat (limited to 'wttrin-geolocation.el')
-rw-r--r--wttrin-geolocation.el114
1 files changed, 103 insertions, 11 deletions
diff --git a/wttrin-geolocation.el b/wttrin-geolocation.el
index 03b573a..9f00b7f 100644
--- a/wttrin-geolocation.el
+++ b/wttrin-geolocation.el
@@ -46,6 +46,11 @@
(require 'json)
(require 'url)
+;; For the shared error hierarchy (`wttrin-invalid-input' et al.). This is a
+;; sub-module of wttrin and is only ever loaded through it, so the require is a
+;; no-op in practice; it makes the dependency explicit and keeps the condition
+;; symbols defined even if this file is loaded on its own.
+(require 'wttrin)
(defgroup wttrin-geolocation nil
"IP geolocation settings for wttrin."
@@ -65,6 +70,26 @@ can select them here."
(const :tag "ipwho.is" ipwhois)
(symbol :tag "Other (registered in wttrin-geolocation--providers)")))
+(defcustom wttrin-geolocation-command nil
+ "Optional shell command for higher-accuracy geolocation.
+When non-nil, `wttrin-geolocation-detect' runs this command and reads its
+standard output as JSON, expecting numeric `lat' and `lng' keys (any other
+keys are ignored). The detected location resolves to \"LAT,LNG\", which
+wttr.in accepts and echoes a place name for.
+
+This is the opt-in accuracy path. The command may do whatever the system
+supports (a WiFi scan, a GPS read) to beat IP geolocation, and it runs
+asynchronously so a multi-second lookup does not block Emacs. The package
+ships no command and assumes nothing about the OS or network stack, so it is
+inert until set.
+
+On any failure (the command is unset, exits non-zero, or prints no parseable
+lat/lng), wttrin falls back to the IP provider named by
+`wttrin-geolocation-provider'."
+ :group 'wttrin-geolocation
+ :type '(choice (const :tag "None (use IP provider)" nil)
+ (string :tag "Shell command")))
+
;;; Response Parsers
;;
;; Each parser takes a raw JSON string and returns "City, Region" or nil.
@@ -142,7 +167,8 @@ by pushing onto this list; the keys become valid values for
"Return the provider plist for SYMBOL.
Signal an error if SYMBOL is not registered."
(or (cdr (assq symbol wttrin-geolocation--providers))
- (error "Unknown wttrin-geolocation provider: %S" symbol)))
+ (signal 'wttrin-invalid-input
+ (list (format "Unknown wttrin-geolocation provider: %S" symbol)))))
;;; Fetch and Detect
@@ -160,16 +186,34 @@ found. Intended for use inside a `url-retrieve' callback."
(buffer-substring-no-properties (point) (point-max))
'utf-8))))))
-(defun wttrin-geolocation-detect (callback)
- "Detect current location via the configured geolocation provider.
-CALLBACK is invoked asynchronously with a single argument: a
-\"City, Region\" string on success, or nil on any failure (network
-error, HTTP 4xx or 5xx, malformed response, missing fields, or
-provider-specific rate-limit signals).
-
-The provider is selected by `wttrin-geolocation-provider'. Signals
-an error synchronously if that value is not registered in
-`wttrin-geolocation--providers'."
+(defun wttrin-geolocation--parse-coordinates (json-string)
+ "Parse JSON-STRING for numeric `lat'/`lng'; return \"LAT,LNG\" or nil.
+Any keys beyond `lat' and `lng' are ignored. Returns nil on malformed
+JSON or missing/non-numeric coordinates. wttr.in accepts the \"LAT,LNG\"
+form directly and echoes a place name for it."
+ (let ((data (wttrin-geolocation--decode-json json-string)))
+ (when data
+ (let ((lat (cdr (assq 'lat data)))
+ (lng (cdr (assq 'lng data))))
+ (when (and (numberp lat) (numberp lng))
+ (format "%s,%s" lat lng))))))
+
+(defun wttrin-geolocation--parse-address (json-string)
+ "Return the `address' (or `label') string from JSON-STRING, or nil.
+A command may include a human-readable place name alongside its
+coordinates; wttrin shows it in the weather buffer. Returns nil on
+malformed JSON or a missing or empty value."
+ (let ((data (wttrin-geolocation--decode-json json-string)))
+ (when data
+ (let ((address (or (cdr (assq 'address data))
+ (cdr (assq 'label data)))))
+ (when (and (stringp address) (> (length address) 0))
+ address)))))
+
+(defun wttrin-geolocation--detect-via-ip (callback)
+ "Detect location via the IP provider; invoke CALLBACK with the result.
+CALLBACK receives a \"City, Region\" string on success or nil on failure.
+The provider is selected by `wttrin-geolocation-provider'."
(let* ((provider (wttrin-geolocation--lookup-provider
wttrin-geolocation-provider))
(url (plist-get provider :url))
@@ -186,5 +230,53 @@ an error synchronously if that value is not registered in
(ignore-errors (kill-buffer (current-buffer)))
(funcall callback result))))))
+(defun wttrin-geolocation--detect-via-command (callback)
+ "Run `wttrin-geolocation-command' asynchronously; invoke CALLBACK with result.
+CALLBACK is called with (COORDS ADDRESS): COORDS is a \"LAT,LNG\" string when
+the command exits zero and prints JSON with numeric `lat'/`lng', and ADDRESS
+is the optional human-readable place name from the JSON (nil when absent).
+On any failure (spawn error, non-zero exit, or unparseable output) CALLBACK
+is called with nil."
+ (let ((output ""))
+ (condition-case nil
+ (make-process
+ :name "wttrin-geolocation"
+ :command (list shell-file-name shell-command-switch
+ wttrin-geolocation-command)
+ :connection-type 'pipe
+ :noquery t
+ :filter (lambda (_proc chunk) (setq output (concat output chunk)))
+ :sentinel (lambda (proc _event)
+ (when (memq (process-status proc) '(exit signal))
+ (let* ((ok (eq (process-exit-status proc) 0))
+ (coords (and ok (wttrin-geolocation--parse-coordinates
+ output)))
+ (address (and coords
+ (wttrin-geolocation--parse-address
+ output))))
+ (funcall callback coords address)))))
+ (error (funcall callback nil)))))
+
+(defun wttrin-geolocation-detect (callback)
+ "Detect current location; invoke CALLBACK asynchronously with the result.
+CALLBACK is called with (LOCATION &optional ADDRESS). LOCATION is a location
+string (\"City, Region\" from an IP provider, or \"LAT,LNG\" from a command) on
+success, or nil on any failure. ADDRESS is the optional human-readable place
+name a command may supply alongside coordinates (always nil on the IP path).
+Callers that only need the location may accept a single argument.
+
+When `wttrin-geolocation-command' is non-nil, run it first; on success its
+coordinates (and address, if any) are used, and on failure detection falls back
+to the IP provider named by `wttrin-geolocation-provider'. When no command is
+set, the IP provider is used directly. Signals an error synchronously if
+`wttrin-geolocation-provider' is not registered and the IP path is reached."
+ (if wttrin-geolocation-command
+ (wttrin-geolocation--detect-via-command
+ (lambda (coords &optional address)
+ (if coords
+ (funcall callback coords address)
+ (wttrin-geolocation--detect-via-ip callback))))
+ (wttrin-geolocation--detect-via-ip callback)))
+
(provide 'wttrin-geolocation)
;;; wttrin-geolocation.el ends here