diff options
Diffstat (limited to 'wttrin-geolocation.el')
| -rw-r--r-- | wttrin-geolocation.el | 114 |
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 |
