diff --git a/elisp/chronometrist.el b/elisp/chronometrist.el index 41e581b..bdc498b 100644 --- a/elisp/chronometrist.el +++ b/elisp/chronometrist.el @@ -2052,7 +2052,7 @@ using `chronometrist-details-schema-transformers'.") "Return rows to be displayed in the `chronometrist-details' buffer. Return value is a list as specified by `tabulated-list-entries'." (cl-loop with index = 1 - for plist in (chronometrist-details-intervals-for-range chronometrist-details-range chronometrist-events) + for plist in (chronometrist-details-intervals chronometrist-details-range chronometrist-details-filter chronometrist-events) collect (-let* (((&plist :name name :tags tags :start start :stop stop) plist) ;; whether tags or key-values are actually displayed is handled later @@ -2060,18 +2060,18 @@ Return value is a list as specified by `tabulated-list-entries'." (key-values (chronometrist-details-rows-helper plist)) ;; resetting seconds with `ts-apply' is necessary to ;; prevent situations like "1 hour from 00:08 to 01:09" - (start (ts-apply :second 0 - (chronometrist-iso-timestamp-to-ts start))) - (stop (ts-apply :second 0 - (if stop - (chronometrist-iso-timestamp-to-ts stop) - (ts-now)))) + (start (ts-apply :second 0 (chronometrist-iso-timestamp-to-ts start))) + (stop (ts-apply :second 0 (if stop + (chronometrist-iso-timestamp-to-ts stop) + (ts-now)))) (interval (floor (ts-diff stop start))) (index-string (format "%s" index)) (duration (chronometrist-format-duration-long interval)) - (timespan (format "from %s to %s" - (ts-format chronometrist-details-time-format-string start) - (ts-format chronometrist-details-time-format-string stop)))) + (timespan (format "from %s to %s" + (ts-format chronometrist-details-time-format-string + start) + (ts-format chronometrist-details-time-format-string + stop)))) (--> (vconcat (vector index-string name) (when chronometrist-details-display-tags (vector tags)) (when chronometrist-details-display-key-values (vector key-values)) @@ -2082,9 +2082,14 @@ Return value is a list as specified by `tabulated-list-entries'." (defvar chronometrist-details-mode-map (let ((map (make-sparse-keymap))) - (define-key map "R" 'chronometrist-details-set-range) + (define-key map (kbd "s r") 'chronometrist-details-set-range) + (define-key map (kbd "s f") 'chronometrist-details-set-filter) map)) +(easy-menu-define chronometrist-details-menu chronometrist-details-mode-map + "Menu for `chronometrist-details'." + '("Details" ["Set date/time range" chronometrist-details-set-range])) + (define-derived-mode chronometrist-details-mode tabulated-list-mode "Details" "Major mode for `chronometrist-details'." (make-local-variable 'tabulated-list-format) @@ -2152,12 +2157,21 @@ TABLE must be a hash table similar to `chronometrist-events'." (let ((input (completing-read-multiple (concat "Range (blank, ISO-8601 date, " "or two ISO-8601 dates/timestamps): ") - (hash-table-keys chronometrist-events) nil nil chronometrist-details-range))) + (reverse (hash-table-keys chronometrist-events)) nil nil + (pcase chronometrist-details-range + ('nil nil) + ((pred stringp) + (format "%s" chronometrist-details-range)) + (`(,begin . ,end) + (format "%s,%s" begin end)))))) (pcase input - ("" (setq-local chronometrist-details-range nil)) - ((or string `(,begin . ,end)) - (setq-local chronometrist-details-range (read input))) - (t (error "Unsupported range."))))) + ('nil (setq-local chronometrist-details-range nil)) + (`(,date) + (setq-local chronometrist-details-range date)) + (`(,begin ,end) + (setq-local chronometrist-details-range (cons begin end))) + (_ (error "Unsupported range."))) + (tabulated-list-revert))) (defvar chronometrist-details-filter nil "Parameters to filter intervals displayed by `chronometrist-details'. @@ -2169,8 +2183,46 @@ A predicate of one argument (the interval plist) - display all intervals for which the predicate returns non-nil.") (make-variable-buffer-local 'chronometrist-details-filter) -(defun chronometrist-details-set-filter (arg) - "Prompt user for filter for current `chronometrist-details' buffer.") +(defun chronometrist-details-filter-match-p (plist filter) + "Return PLIST if it matches FILTER. +FILTER must be a filter specifier as described by +`chronometrist-details-filter'." + (cond ((null filter) plist) + ((seq-every-p #'keywordp filter) + (when (--map (plist-get plist keyword) filter) + plist)) + ((chronometrist-plist-p filter) + (when (cl-loop for (keyword value) on filter by #'cddr + always (equal (plist-get plist keyword) value)) + plist)) + ((functionp filter) + (when (funcall filter plist) plist)) + (t (error "Invalid filter.")))) + +(defun chronometrist-details-set-filter () + "Prompt user for filter for current `chronometrist-details' buffer." + (interactive) + (let* ((input (completing-read + (concat "Filter (blank, one or more keywords, " + "a plist, or a predicate): ") + nil nil nil + (pcase chronometrist-details-filter + ('nil "") + ((pred consp) (format "%S" chronometrist-details-filter))))) + (sexp (ignore-errors (read input)))) + (cond ((equal input "") (setq-local chronometrist-details-filter nil)) + ((consp sexp) (setq-local chronometrist-details-filter sexp)) + (_ (error "Unsupported filter."))) + (tabulated-list-revert))) + +(defun chronometrist-details-intervals (range filter table) + "Return plists matching RANGE and FILTER from TABLE. +For values of RANGE, see `chronometrist-details-range'. For +values of FILTER, see `chronometrist-details-filter'. TABLE must +be a hash table similar to `chronometrist-events'." + (cl-loop for plist in (chronometrist-details-intervals-for-range range table) + when (chronometrist-details-filter-match-p plist filter) + collect plist)) (provide 'chronometrist) diff --git a/elisp/chronometrist.org b/elisp/chronometrist.org index b3ac5d6..fc5d26c 100644 --- a/elisp/chronometrist.org +++ b/elisp/chronometrist.org @@ -2792,7 +2792,7 @@ Argument _FS-EVENT is ignored." '(change) #'chronometrist-refresh-file)))) #+END_SRC -**** chronometrist-report :command: +**** chronometrist-report :command: #+BEGIN_SRC emacs-lisp ;;;###autoload (defun chronometrist-report (&optional keep-date) @@ -3054,7 +3054,7 @@ value of `revert-buffer-function'." '(change) #'chronometrist-refresh-file)))) #+END_SRC -**** chronometrist-statistics :command: +**** chronometrist-statistics :command: #+BEGIN_SRC emacs-lisp ;;;###autoload (defun chronometrist-statistics (&optional preserve-state) @@ -3282,7 +3282,7 @@ using `chronometrist-details-schema-transformers'.") "Return rows to be displayed in the `chronometrist-details' buffer. Return value is a list as specified by `tabulated-list-entries'." (cl-loop with index = 1 - for plist in (chronometrist-details-intervals-for-range chronometrist-details-range chronometrist-events) + for plist in (chronometrist-details-intervals chronometrist-details-range chronometrist-details-filter chronometrist-events) collect (-let* (((&plist :name name :tags tags :start start :stop stop) plist) ;; whether tags or key-values are actually displayed is handled later @@ -3290,18 +3290,18 @@ Return value is a list as specified by `tabulated-list-entries'." (key-values (chronometrist-details-rows-helper plist)) ;; resetting seconds with `ts-apply' is necessary to ;; prevent situations like "1 hour from 00:08 to 01:09" - (start (ts-apply :second 0 - (chronometrist-iso-timestamp-to-ts start))) - (stop (ts-apply :second 0 - (if stop - (chronometrist-iso-timestamp-to-ts stop) - (ts-now)))) + (start (ts-apply :second 0 (chronometrist-iso-timestamp-to-ts start))) + (stop (ts-apply :second 0 (if stop + (chronometrist-iso-timestamp-to-ts stop) + (ts-now)))) (interval (floor (ts-diff stop start))) (index-string (format "%s" index)) (duration (chronometrist-format-duration-long interval)) - (timespan (format "from %s to %s" - (ts-format chronometrist-details-time-format-string start) - (ts-format chronometrist-details-time-format-string stop)))) + (timespan (format "from %s to %s" + (ts-format chronometrist-details-time-format-string + start) + (ts-format chronometrist-details-time-format-string + stop)))) (--> (vconcat (vector index-string name) (when chronometrist-details-display-tags (vector tags)) (when chronometrist-details-display-key-values (vector key-values)) @@ -3315,17 +3315,18 @@ Return value is a list as specified by `tabulated-list-entries'." #+BEGIN_SRC emacs-lisp (defvar chronometrist-details-mode-map (let ((map (make-sparse-keymap))) - (define-key map "R" 'chronometrist-details-set-range) + (define-key map (kbd "s r") 'chronometrist-details-set-range) + (define-key map (kbd "s f") 'chronometrist-details-set-filter) map)) #+END_SRC -**** menu +**** chronometrist-details-menu :menu: #+BEGIN_SRC emacs-lisp (easy-menu-define chronometrist-details-menu chronometrist-details-mode-map "Menu for `chronometrist-details'." '("Details" ["Set date/time range" chronometrist-details-set-range])) #+END_SRC -**** chronometrist-details-mode :major:mode: +**** chronometrist-details-mode :major:mode: #+BEGIN_SRC emacs-lisp (define-derived-mode chronometrist-details-mode tabulated-list-mode "Details" "Major mode for `chronometrist-details'." @@ -3366,7 +3367,7 @@ Dates are inclusive.") (make-variable-buffer-local 'chronometrist-details-range) #+END_SRC -**** iso-date-p +**** iso-date-p :function: #+BEGIN_SRC emacs-lisp (defun chronometrist-iso-date-p (string) (string-match-p @@ -3403,7 +3404,7 @@ TABLE must be a hash table similar to `chronometrist-events'." ;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") chronometrist-events) #+END_SRC -**** set-range :command: +**** set-range :command:writer: #+BEGIN_SRC emacs-lisp (defun chronometrist-details-set-range () "Prompt user for range for current `chronometrist-details' buffer." @@ -3411,7 +3412,7 @@ TABLE must be a hash table similar to `chronometrist-events'." (let ((input (completing-read-multiple (concat "Range (blank, ISO-8601 date, " "or two ISO-8601 dates/timestamps): ") - (hash-table-keys chronometrist-events) nil nil + (reverse (hash-table-keys chronometrist-events)) nil nil (pcase chronometrist-details-range ('nil nil) ((pred stringp) @@ -3441,13 +3442,56 @@ intervals for which the predicate returns non-nil.") (make-variable-buffer-local 'chronometrist-details-filter) #+END_SRC -**** set-filter :command: +**** filter-match-p :function: #+BEGIN_SRC emacs-lisp -(defun chronometrist-details-set-filter (arg) - "Prompt user for filter for current `chronometrist-details' buffer.") +(defun chronometrist-details-filter-match-p (plist filter) + "Return PLIST if it matches FILTER. +FILTER must be a filter specifier as described by +`chronometrist-details-filter'." + (cond ((null filter) plist) + ((seq-every-p #'keywordp filter) + (when (--map (plist-get plist keyword) filter) + plist)) + ((chronometrist-plist-p filter) + (when (cl-loop for (keyword value) on filter by #'cddr + always (equal (plist-get plist keyword) value)) + plist)) + ((functionp filter) + (when (funcall filter plist) plist)) + (t (error "Invalid filter.")))) #+END_SRC +**** set-filter :command: +#+BEGIN_SRC emacs-lisp +(defun chronometrist-details-set-filter () + "Prompt user for filter for current `chronometrist-details' buffer." + (interactive) + (let* ((input (completing-read + (concat "Filter (blank, one or more keywords, " + "a plist, or a predicate): ") + nil nil nil + (pcase chronometrist-details-filter + ('nil "") + ((pred consp) (format "%S" chronometrist-details-filter))))) + (sexp (ignore-errors (read input)))) + (cond ((equal input "") (setq-local chronometrist-details-filter nil)) + ((consp sexp) (setq-local chronometrist-details-filter sexp)) + (_ (error "Unsupported filter."))) + (tabulated-list-revert))) +#+END_SRC +**** intervals :function: +#+BEGIN_SRC emacs-lisp +(defun chronometrist-details-intervals (range filter table) + "Return plists matching RANGE and FILTER from TABLE. +For values of RANGE, see `chronometrist-details-range'. For +values of FILTER, see `chronometrist-details-filter'. TABLE must +be a hash table similar to `chronometrist-events'." + (cl-loop for plist in (chronometrist-details-intervals-for-range range table) + when (chronometrist-details-filter-match-p plist filter) + collect plist)) + +#+END_SRC ** Provide #+BEGIN_SRC emacs-lisp (provide 'chronometrist)