diff --git a/biome-api-parse.el b/biome-api-parse.el index 8c9a68f..491edc4 100644 --- a/biome-api-parse.el +++ b/biome-api-parse.el @@ -29,6 +29,7 @@ ;;; Code: (require 'browse-url) +(require 'dom) (require 'cl-lib) (require 'seq) diff --git a/biome-api.el b/biome-api.el index eaeb197..fa0cc78 100644 --- a/biome-api.el +++ b/biome-api.el @@ -22,7 +22,7 @@ ;;; Commentary: -;; TODO +;; Make requests to Open Meteo API. ;;; Code: (require 'json) @@ -46,14 +46,13 @@ "Default URLs for Open Meteo API.") (defcustom biome-api-urls biome-api--default-urls - "URL of the Open Meteo API." + "URLs of the Open Meteo API." :type `(alist :key-type string :value-type string :options ,(mapcar #'car biome-api--default-urls)) :group 'biome) - (defun biome-api--get-params (query) "Convert QUERY to a list of parameters for `request'. @@ -87,7 +86,7 @@ QUERY is a form as defined by `biome-query-current'." "Keymap for `biome-api-error-mode'.") (define-derived-mode biome-api-error-mode text-mode "Lyrics view" - "Major mode for viewing open meteo errors. + "Major mode for viewing open meteo API errors. \\{biome-api-error-mode-map}" (read-only-mode 1)) diff --git a/biome-grid.el b/biome-grid.el index ff2c9a5..157b68e 100644 --- a/biome-grid.el +++ b/biome-grid.el @@ -22,7 +22,7 @@ ;;; Commentary: -;; TODO +;; Display results of open meteo queries with `tabulated-list'. ;;; Code: (require 'cl-lib) @@ -38,6 +38,9 @@ :group 'biome) (defun biome-grid--format-units (format) + "Add derived units to FORMAT. + +FORMAT is an expression as defined by `biome-grid-format'." (nreverse (reduce (lambda (acc def) (push def acc) @@ -278,8 +281,29 @@ (nil "is_day" is-day) ("wmo code" nil wmo-code) ("°" nil direction))) - "Format units in the grid." - :group 'biome) + "Format units in the grid. + +This is a list with the following keys: +- unit; +- parameter (regexp); +- format defintion. + +The format definition is one of the following: +- (gradient (value . color) ...); +- `is-day'; +- `wmo-code'; +- `direction'." + :group 'biome + :type '(repeat (list + (choice :tag "Unit" (const :tag "No unit" nil) string) + (choice :tag "Parameter (regexp)" (const :tag "No parameter" nil) string) + (choice + (cons (const gradient) + (alist :key-type (number :tag "Value") + :value-type (string :tag "HEX color"))) + (const :tag "Is day" is-day) + (const :tag "WMO code" wmo-code) + (const :tag "Direction" direction))))) (defcustom biome-grid-wmo-codes '((0 "☀️" "Clear sky") @@ -312,7 +336,7 @@ (99 "⛈️" "Thunderstorm with hail (heavy)")) "Descriptions for WMO weather codes. -The defaults values are takes from open-meteo docs." +The defaults values are taken from open-meteo docs." :group 'biome :type '(alist :key-type number :value-type (list string string))) @@ -336,12 +360,13 @@ The defaults values are takes from open-meteo docs." (360 "↑ N")) "Descriptions for directions." :group 'biome - :type '(repeat (list number string))) + :type '(repeat (list (number :tag "Value (0-360)") + (string :tag "Description")))) (defcustom biome-grid-is-day-format '("🌙 night" "☀️ day") "Format for is-day values." :group 'biome - :type '(repeat string)) + :type '(list (string :tag "Night") (string :tag "Day"))) (defcustom biome-grid-wmo-show-emoji t "Show emoji for WMO weather codes." @@ -371,6 +396,10 @@ C1 and C2 are hex RGS strings, VAL is a number between 0 and 1." collect (+ (* (- 1 val) v1) (* val v2)))))) (defun biome-grid--format-gradient (value def col-width) + "Format VALUE with gradient DEF. + +DEF is a list of (value . color) pairs. COL-WIDTH is the width of the +column." (setq value (float value)) (let* ((background-color (cl-loop for (border1 . color1) in def @@ -388,7 +417,7 @@ C1 and C2 are hex RGS strings, VAL is a number between 0 and 1." (defun biome-grid--format-wmo-code (value) "Format WMO code. -VALUE is a WMO number." +VALUE is a WMO number as defined in `biome-grid-wmo-codes'." (let ((format (alist-get value biome-grid-wmo-codes))) (if format (if biome-grid-wmo-show-emoji @@ -415,12 +444,21 @@ VALUE is 0 or 1." (nth value biome-grid-is-day-format)) (defun biome-grid--get-format-def (col-key unit) + "Get format definition for COL-KEY and UNIT." (cl-loop for (unit-def col-key-def format-def) in biome-grid-format if (and (or (null unit-def) (equal unit unit-def)) - (or (null col-key-def) (string-match-p col-key-def (symbol-name col-key)))) + (or (null col-key-def) (string-match-p col-key-def + (symbol-name col-key)))) return format-def)) (defun biome-grid--prepare-entries (entries col-key unit) + "Prepare entries for display. + +This function performs all changes that may affect the width of the +column. + +ENTRIES is a list of values. COL-KEY is the column key as given by +the API. UNIT is the unit of the column." (let ((format-def (biome-grid--get-format-def col-key unit))) (mapcar (lambda (entry) @@ -436,6 +474,13 @@ VALUE is 0 or 1." entries))) (defun biome-grid--format-entries (entries col-key unit col-width) + "Format entries for display. + +This uses the width COL-WIDTH for each column, therefore it must not change +that width. + +ENTRIES is a list of values. COL-KEY is the column key as given by +the API. UNIT is the unit of the column." (let ((format-def (biome-grid--get-format-def col-key unit))) (mapcar (lambda (entry) @@ -448,6 +493,9 @@ VALUE is 0 or 1." entries))) (defun biome-grid--get-col-witdh (col-name entries unit) + "Get the width of the column COL-NAME. + +ENTRIES is a list of values. UNIT is the unit of the column." (let ((col-name-width (length col-name)) (entry-width (or (cl-loop for entry in entries @@ -457,10 +505,15 @@ VALUE is 0 or 1." ;; XXX this is necessary to compensate for emojis of different ;; actual width. Forunately this doesn't break the formmating of ;; the grid (hail `tabulated-list', what a pleasant surprise) - (cond ((and (equal unit "wmo code") biome-grid-wmo-show-emoji) (max col-name-width (+ 1 entry-width))) + (cond ((and (equal unit "wmo code") biome-grid-wmo-show-emoji) + (max col-name-width (+ 1 entry-width))) (t (max col-name-width (+ 1 entry-width)))))) (defun biome-grid--set-list (query results) + "Set the list of the grid for the `biome-grid-mode' buffer. + +QUERY is a form as defined by `biome-query-current'. RESULTS is a +response of the API." (let* ((group (intern (alist-get :group query))) (group-units (intern (format "%s_units" (alist-get :group query)))) (var-names (biome-query--get-var-names-cache)) @@ -514,6 +567,10 @@ it, in which case it is killed." (kill-buffer))) (defun biome-grid--update-columns (columns) + "Update the columns of the grid. + +COLUMNS is a list of column names prefixed with \"--\" (to work with +transient switches)." (interactive (list (transient-args transient-current-command))) (setq biome-grid--columns-display (cl-loop for (key name _display) in biome-grid--columns-display @@ -523,7 +580,7 @@ it, in which case it is killed." (tabulated-list-init-header)) (transient-define-prefix biome-grid-columns () - "Toggle columns in biome-grid buffer." + "Toggle columns in `biome-grid' buffer." ["Toggle columns" :setup-children (lambda (_) @@ -558,13 +615,19 @@ it, in which case it is killed." "Keymap for `biome-grid-mode'.") (define-derived-mode biome-grid-mode tabulated-list-mode "Biome Grid" - "Major mode for displaying biome results.") + "Major mode for displaying biome results. + +\\{biome-grid-mode-map}") (defun biome-grid (query results) "Display RESULTS in a grid. QUERY is a form as defined by `biome-query-current'. RESULTS is a -response of Open Meteo (returned by `biome-api-get'." +response of Open Meteo (returned by `biome-api-get'. + +The grid is displayed with `tabulated-list'. It doesn't support +displaying more columns than the window width, so there's +`biome-grid-columns' to toggle columns." (let ((buf (generate-new-buffer "*biome-grid*"))) (with-current-buffer buf (biome-grid-mode) diff --git a/biome-query.el b/biome-query.el index 48e90ec..3c43b20 100644 --- a/biome-query.el +++ b/biome-query.el @@ -22,7 +22,12 @@ ;;; Commentary: -;; TODO +;; Query editor for Open Meteo API. +;; +;; The main entrypoints are: +;; - `biome-query' - start a new query. +;; - `biome-query--section-open' - start a new query in a section. +;; These aren't meant to be used directly by the user. ;;; Code: (require 'biome-api-data) @@ -42,7 +47,7 @@ :group 'biome) (defcustom biome-query-date-format "%A, %x" - "Format string for date entries. + "Format string for date entries in query buffers. By default \"WEEKDAY, DATE\", where DATE is what Emacs thinks is an appropriate way to format days in your language. @@ -56,10 +61,13 @@ value will be inserted." (defcustom biome-query-coords '(("Helsinki, Finland" 60.16952 24.93545) ("Berlin, Germany" 52.52437 13.41053) ("Dubai, UAE" 25.0657 55.17128)) - "List of locations with their coordinates. + "List of saved locations with their coordinates. The format is: (name latitude longitude)." - :type '(repeat (list string number number)) + :type '(repeat (list + (string :tag "Name") + (number :tag "Latitude") + (number :tag "Longitude"))) :group 'biome) (defconst biome-query-groups '("daily" "hourly" "minutely_15" "hourly") @@ -80,20 +88,18 @@ have to be displayed separately.") "Items to ignore when generating unique keys.") (defvar biome-query-current nil - "Current report. + "Current query. It is an alist with the following keys: - `:name' - name of the root section. - `:group' - name of the group (see `biome-query-groups'). -- `:params' - alist with parameters, where the key is either nil (for - global parameters) or the value of `:param' key of the corresponding - section. - -In the former case, the value is an alist with values; in the latter -case, the value is a list of variable names available in the group.") +- `:params' - alist with parameters, where the key is either the + parameter name (for global variables) or the group name (for grouped + variables, e.g. hourly, daily). In the latter case, the value has + to be a list of grouped variables names.") (defvar biome-query--current-section nil - "Current section.") + "Current section definition.") (defvar biome-query--layout-cache (make-hash-table :test 'equal) "Cache for dynamic transient layout.") @@ -104,19 +110,22 @@ case, the value is a list of variable names available in the group.") (defvar biome-query--callback nil "Call this with the selected query.") -;; TODO delete this -(setq biome-query--layout-cache (make-hash-table :test 'equal)) - ;; Transient display classes (defclass biome-query--transient-report (transient-suffix) ((transient :initform t)) - "A transient class to display the current report.") + "A transient class to display the current report. + +This just prints out `biome-query-current' in somewhat readable +form.") (cl-defmethod transient-init-value ((_ biome-query--transient-report)) "A dummy method for `biome-query--transient-report'." nil) (defun biome-query--update-names-cache (sections cache) + "Update the variable names cache for SECTIONS. + +CACHE is a hash table. See `biome-query--get-var-names-cache'." (cl-loop for section in sections do (when-let (fields (alist-get :fields section)) (cl-loop for (api-key . params) in fields @@ -126,6 +135,14 @@ case, the value is a list of variable names available in the group.") (biome-query--update-names-cache children cache)))) (defun biome-query--get-var-names-cache () + "Generate variable names cache for the current section. + +The current section is determined by `biome-query-current'. The +return value is a hashmap with variable api keys as keys and their +names as values. + +This is useful because otherwise it would take a long time to look for +disambiguations of parameters in `biome-api-data'." (let* ((name (alist-get :name biome-query-current)) (cache (alist-get name biome-query--var-names-cache nil nil #'equal))) @@ -139,12 +156,16 @@ case, the value is a list of variable names available in the group.") cache)))) (defun biome-query--get-header (key var-names) + "Generate readable name for KEY with a fallback. + +KEY is the api key of the variable. VAR-NAMES is the output of +`biome-query--get-var-names-cache'." (gethash key var-names (capitalize (replace-regexp-in-string (regexp-quote "_") " " key)))) (cl-defmethod transient-format ((_ biome-query--transient-report)) - "Format the current report." + "Format the `biome-query-current'." (let ((group (alist-get :group biome-query-current)) (var-names (biome-query--get-var-names-cache)) lat lon group-vars line-vars vars) @@ -221,14 +242,14 @@ case, the value is a list of variable names available in the group.") (defclass biome-query--transient-path (transient-suffix) ((transient :initform t)) - "A transient class to display the current path.") + "A transient class to display the current path in query.") (cl-defmethod transient-init-value ((_ biome-query--transient-path)) "A dummy method for `biome-query--transient-report'." nil) (cl-defmethod transient-format ((_ biome-query--transient-path)) - "Format the current path." + "Format the current path in query." (let* ((scope (oref (or transient--prefix transient-current-prefix) scope)) @@ -298,10 +319,7 @@ OBJ is an instance of `biome-query--transient-switch-variable'." new-value)) (cl-defmethod transient-format ((obj biome-query--transient-switch-variable)) - "Return a string generated using OBJ's `format'. -%k is formatted using `transient-format-key'. -%d is formatted using `transient-format-description'. -%v is formatted using `transient-format-value'." + "Format the variable switch OBJ." (concat " " (string-pad (transient-format-key obj) 6) @@ -311,12 +329,14 @@ OBJ is an instance of `biome-query--transient-switch-variable'." (defclass biome-query--transient-variable (transient-variable) ((api-key :initarg :api-key)) - "A transient class to display a variable.") + "A transient class to work with query variables. + +This is mostly used to interact with `biome-query-current'.") (cl-defmethod transient-infix-set ((obj biome-query--transient-variable) value) "Set the value of OBJ to VALUE. -OBJ is an instance of `biome-query--transient-date-variable'." +This also updates `biome-query-current' with the new value." (if value (setf (alist-get (oref obj api-key) (alist-get :params biome-query-current) @@ -347,9 +367,15 @@ OBJ is an instance of `biome-query--transient-select-variable'." (propertize "unset" 'face 'transient-inactive-value)))) (defclass biome-query--transient-date-variable (biome-query--transient-variable) () - "A transient class to display a date variable.") + "A transient class to work with a date variable. + +The variable is read via `org-read-date' and stored as a UNIX +timestamp.") (cl-defmethod transient-infix-read ((obj biome-query--transient-date-variable)) + "Read a date from the user with `org-read-date'. + +OBJ is an instance of `biome-query--transient-date-variable'." (unless (oref obj value) (let ((org-read-date-force-compatible-dates nil)) (time-convert @@ -364,7 +390,6 @@ OBJ is an instance of `biome-query--transient-date-variable'." (if value (propertize (format-time-string - ;; TODO fix biome-query-date-format (seconds-to-time value)) @@ -372,9 +397,16 @@ OBJ is an instance of `biome-query--transient-date-variable'." (propertize "unset" 'face 'transient-inactive-value)))) (defclass biome-query--transient-select-variable (biome-query--transient-variable) - ((options :initarg :options))) + ((options :initarg :options)) + "A transient class to display a select variable. + +`:options' is an alist of the form ((KEY . DESCRIPTION) ...). If the +number of options is more than +`biome-query-completing-read-threshold', the user is prompted via +`completing-read'. Otherwise, it just switches to the next option.") (cl-defmethod transient-infix-value ((obj biome-query--transient-select-variable)) + "Return the value of OBJ." (oref obj value)) (cl-defmethod transient-format-value ((obj biome-query--transient-select-variable)) @@ -417,7 +449,7 @@ OBJ is an instance of `biome-query--transient-date-variable'." ((min :initarg :min :initform nil) (max :initarg :max :initform nil) (integer :initarg :integer :initform nil)) - "A transient class to display a number variable.") + "A transient class to work with a number variable.") (cl-defmethod transient-infix-read ((obj biome-query--transient-number-variable)) "Read the value of OBJ." @@ -475,7 +507,7 @@ OBJ is an instance of `biome-query--transient-date-variable'." (propertize "unset" 'face 'transient-inactive-value)))) (defclass biome-query--transient-timezone-variable (biome-query--transient-variable) () - "A transient class to display a timezone variable.") + "A transient class to work with a timezone variable.") (cl-defmethod transient-infix-read ((obj biome-query--transient-timezone-variable)) "Read the value of OBJ." @@ -483,7 +515,9 @@ OBJ is an instance of `biome-query--transient-date-variable'." nil t (oref obj value))) (defclass biome-query--transient-coords (biome-query--transient-variable) () - "A transient class for a coordinate switcher.") + "A transient class for a coordinate switcher. + +The source of possible coordinates is `biome-query-coords'.") (cl-defmethod transient-init-value ((obj biome-query--transient-coords)) "Initialize the value of OBJ." @@ -535,15 +569,19 @@ OBJ is an instance of `biome-query--transient-date-variable'." "A transient class to switch between groups of a query.") (cl-defmethod transient-infix-value ((obj biome-query--transient-group-switch)) + "Return the value of OBJ." (oref obj value)) (cl-defmethod transient-init-value ((obj biome-query--transient-group-switch)) + "Initialize the value of OBJ." (let ((groups (biome-query--section-groups biome-query--current-section))) (oset obj options groups) (oset obj value (alist-get :group biome-query-current)))) (cl-defmethod transient-infix-read ((obj biome-query--transient-group-switch)) - "Read the value of OBJ." + "Switch to the next group. + +OBJ is an instance of `biome-query--transient-group-switch'." (let* ((options (mapcar (lambda (c) (cons (cdr c) (car c))) (oref obj options))) @@ -591,7 +629,7 @@ number of symbols from the start of the corresponding word. SEQ-LENGTHS is a list of possible values of IT - if it's 2, then the item is a number that can only be taken as a whole, otherwise it's the length of the word." - ;; TODO better weight function + ;; TODO better weight function? (cl-loop for take in it for length in seq-lengths if (= 2 length) sum 1 @@ -601,7 +639,7 @@ it's the length of the word." "Generate unique key candidates for NAME. The algorithm is as follows: NAME is split into words, each word -produces a list of all its prefixes. E.g. \"hello\" produces \"\", +produces a list of all its prefixes. E.g. \"hello\" produces \"\", \"h\", \"he\", \"hel\", etc. Numbers are takes as a whole, e.g. \"100\" produces just \"\" and \"100\". @@ -639,8 +677,6 @@ at 3." (reverse sequences) (reduce #'biome-query--cartesian-product) (mapcar (lambda (it) (if (listp it) (nreverse it) (list it)))) - ;; TODO delete comment - ;; ((lambda (kek) (message "Sorting %s" (length kek)) kek)) ;; XXX this seems to be just a bit faster than `seq-sort-by'. (mapcar (lambda (it) (cons (biome-query--unique-key-weight it seq-lengths) it))) @@ -706,6 +742,9 @@ exclude from the result." (defun biome--query-section-fields-define-infixes (fields keys param infix-name) "Define infixes for FIELDS. +I wish it were possible to avoid that... Maybe I just didn't find the +correct way. + PARAM is the value of `:param' of the section. INFIX-NAME is the prefix for infix names. KEYS is a hash table mapping field names to keys." @@ -761,7 +800,7 @@ to keys." :argument ,name)))))) (defun biome-query--section-fields-children (fields keys parents cache-key) - "Get transient laoyut for FIELDS. + "Get transient layout for FIELDS. FIELDS is a list of fields as defined in `biome-api-parse--page'. KEYS is the result of `biome-query--unique-keys'. PARENTS is a list @@ -822,9 +861,14 @@ list of parent sections." sections)]))) (defmacro biome-query--with-layout-cache (cache-key &rest body) - "Cache layout for CACHE-KEY. + "Cache layout for CACHE-KEY in `biome-query--layout-cache'. -BODY is the body of the macro." +BODY is the body of the macro that returns the layout. The resulting +expression either returns the cached layout or evaluates BODY, caches +the result and returns it. + +This is necessary mostly because `biome-query--unique-keys' is +sometimes a bit slow." (declare (indent 1)) `(let ((layout (gethash ,cache-key biome-query--layout-cache))) (if layout @@ -878,6 +922,7 @@ SUFFIXES is a list of suffix definitions." collect (cons group (alist-get :name child)))) (defun biome-query--reset-report () + "Reset the current query to the current section." (interactive) (setq biome-query-current (copy-tree @@ -893,6 +938,7 @@ SUFFIXES is a list of suffix definitions." (funcall biome-query--callback biome-query-current)) (defun biome-query--generate-preset () + "Generate a preset for the current query." (interactive) (let ((buf (generate-new-buffer "*biome-preset*"))) (with-current-buffer buf @@ -931,6 +977,7 @@ SECTION is a form as defined in `biome-api-parse--page'." (put 'biome-query-section 'transient--layout nil))) (defun biome-query--section-open (name) + "Open section NAME in `biome-query--section'." (let ((params (alist-get name biome-api-data nil nil #'equal))) (unless params (error "No such section: %s" name)) diff --git a/biome.el b/biome.el index af15938..fe5f6e7 100644 --- a/biome.el +++ b/biome.el @@ -25,7 +25,17 @@ ;;; Commentary: -;; TODO +;; Interface to Open Meteo for Emacs. The +;; API provides weather forecasts, historical weather data, climate +;; change projections, and more. +;; +;; The API is free for non-commercial use if you make less than 10000 +;; requests per day. +;; +;; The package has two main entrypoints: `biome' and `biome-resume'. The +;; former starts a new query, the latter resumes the last query. +;; +;; Also check out the README at . ;;; Code: (require 'biome-api)