feat(details): implement filter; refactor `rows`

This commit is contained in:
contrapunctus 2021-06-28 06:08:45 +05:30
parent 5adf3f3901
commit fd468a9ed5
2 changed files with 135 additions and 39 deletions

View File

@ -2052,7 +2052,7 @@ using `chronometrist-details-schema-transformers'.")
"Return rows to be displayed in the `chronometrist-details' buffer. "Return rows to be displayed in the `chronometrist-details' buffer.
Return value is a list as specified by `tabulated-list-entries'." Return value is a list as specified by `tabulated-list-entries'."
(cl-loop with index = 1 (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 collect
(-let* (((&plist :name name :tags tags :start start :stop stop) plist) (-let* (((&plist :name name :tags tags :start start :stop stop) plist)
;; whether tags or key-values are actually displayed is handled later ;; 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)) (key-values (chronometrist-details-rows-helper plist))
;; resetting seconds with `ts-apply' is necessary to ;; resetting seconds with `ts-apply' is necessary to
;; prevent situations like "1 hour from 00:08 to 01:09" ;; prevent situations like "1 hour from 00:08 to 01:09"
(start (ts-apply :second 0 (start (ts-apply :second 0 (chronometrist-iso-timestamp-to-ts start)))
(chronometrist-iso-timestamp-to-ts start))) (stop (ts-apply :second 0 (if stop
(stop (ts-apply :second 0 (chronometrist-iso-timestamp-to-ts stop)
(if stop (ts-now))))
(chronometrist-iso-timestamp-to-ts stop)
(ts-now))))
(interval (floor (ts-diff stop start))) (interval (floor (ts-diff stop start)))
(index-string (format "%s" index)) (index-string (format "%s" index))
(duration (chronometrist-format-duration-long interval)) (duration (chronometrist-format-duration-long interval))
(timespan (format "from %s to %s" (timespan (format "from %s to %s"
(ts-format chronometrist-details-time-format-string start) (ts-format chronometrist-details-time-format-string
(ts-format chronometrist-details-time-format-string stop)))) start)
(ts-format chronometrist-details-time-format-string
stop))))
(--> (vconcat (vector index-string name) (--> (vconcat (vector index-string name)
(when chronometrist-details-display-tags (vector tags)) (when chronometrist-details-display-tags (vector tags))
(when chronometrist-details-display-key-values (vector key-values)) (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 (defvar chronometrist-details-mode-map
(let ((map (make-sparse-keymap))) (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)) 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" (define-derived-mode chronometrist-details-mode tabulated-list-mode "Details"
"Major mode for `chronometrist-details'." "Major mode for `chronometrist-details'."
(make-local-variable 'tabulated-list-format) (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 (let ((input (completing-read-multiple
(concat "Range (blank, ISO-8601 date, " (concat "Range (blank, ISO-8601 date, "
"or two ISO-8601 dates/timestamps): ") "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 (pcase input
("" (setq-local chronometrist-details-range nil)) ('nil (setq-local chronometrist-details-range nil))
((or string `(,begin . ,end)) (`(,date)
(setq-local chronometrist-details-range (read input))) (setq-local chronometrist-details-range date))
(t (error "Unsupported range."))))) (`(,begin ,end)
(setq-local chronometrist-details-range (cons begin end)))
(_ (error "Unsupported range.")))
(tabulated-list-revert)))
(defvar chronometrist-details-filter nil (defvar chronometrist-details-filter nil
"Parameters to filter intervals displayed by `chronometrist-details'. "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.") intervals for which the predicate returns non-nil.")
(make-variable-buffer-local 'chronometrist-details-filter) (make-variable-buffer-local 'chronometrist-details-filter)
(defun chronometrist-details-set-filter (arg) (defun chronometrist-details-filter-match-p (plist filter)
"Prompt user for filter for current `chronometrist-details' buffer.") "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) (provide 'chronometrist)

View File

@ -2792,7 +2792,7 @@ Argument _FS-EVENT is ignored."
'(change) '(change)
#'chronometrist-refresh-file)))) #'chronometrist-refresh-file))))
#+END_SRC #+END_SRC
**** chronometrist-report :command: **** chronometrist-report :command:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
;;;###autoload ;;;###autoload
(defun chronometrist-report (&optional keep-date) (defun chronometrist-report (&optional keep-date)
@ -3054,7 +3054,7 @@ value of `revert-buffer-function'."
'(change) '(change)
#'chronometrist-refresh-file)))) #'chronometrist-refresh-file))))
#+END_SRC #+END_SRC
**** chronometrist-statistics :command: **** chronometrist-statistics :command:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
;;;###autoload ;;;###autoload
(defun chronometrist-statistics (&optional preserve-state) (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 rows to be displayed in the `chronometrist-details' buffer.
Return value is a list as specified by `tabulated-list-entries'." Return value is a list as specified by `tabulated-list-entries'."
(cl-loop with index = 1 (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 collect
(-let* (((&plist :name name :tags tags :start start :stop stop) plist) (-let* (((&plist :name name :tags tags :start start :stop stop) plist)
;; whether tags or key-values are actually displayed is handled later ;; 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)) (key-values (chronometrist-details-rows-helper plist))
;; resetting seconds with `ts-apply' is necessary to ;; resetting seconds with `ts-apply' is necessary to
;; prevent situations like "1 hour from 00:08 to 01:09" ;; prevent situations like "1 hour from 00:08 to 01:09"
(start (ts-apply :second 0 (start (ts-apply :second 0 (chronometrist-iso-timestamp-to-ts start)))
(chronometrist-iso-timestamp-to-ts start))) (stop (ts-apply :second 0 (if stop
(stop (ts-apply :second 0 (chronometrist-iso-timestamp-to-ts stop)
(if stop (ts-now))))
(chronometrist-iso-timestamp-to-ts stop)
(ts-now))))
(interval (floor (ts-diff stop start))) (interval (floor (ts-diff stop start)))
(index-string (format "%s" index)) (index-string (format "%s" index))
(duration (chronometrist-format-duration-long interval)) (duration (chronometrist-format-duration-long interval))
(timespan (format "from %s to %s" (timespan (format "from %s to %s"
(ts-format chronometrist-details-time-format-string start) (ts-format chronometrist-details-time-format-string
(ts-format chronometrist-details-time-format-string stop)))) start)
(ts-format chronometrist-details-time-format-string
stop))))
(--> (vconcat (vector index-string name) (--> (vconcat (vector index-string name)
(when chronometrist-details-display-tags (vector tags)) (when chronometrist-details-display-tags (vector tags))
(when chronometrist-details-display-key-values (vector key-values)) (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 #+BEGIN_SRC emacs-lisp
(defvar chronometrist-details-mode-map (defvar chronometrist-details-mode-map
(let ((map (make-sparse-keymap))) (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)) map))
#+END_SRC #+END_SRC
**** menu **** chronometrist-details-menu :menu:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(easy-menu-define chronometrist-details-menu chronometrist-details-mode-map (easy-menu-define chronometrist-details-menu chronometrist-details-mode-map
"Menu for `chronometrist-details'." "Menu for `chronometrist-details'."
'("Details" ["Set date/time range" chronometrist-details-set-range])) '("Details" ["Set date/time range" chronometrist-details-set-range]))
#+END_SRC #+END_SRC
**** chronometrist-details-mode :major:mode: **** chronometrist-details-mode :major:mode:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(define-derived-mode chronometrist-details-mode tabulated-list-mode "Details" (define-derived-mode chronometrist-details-mode tabulated-list-mode "Details"
"Major mode for `chronometrist-details'." "Major mode for `chronometrist-details'."
@ -3366,7 +3367,7 @@ Dates are inclusive.")
(make-variable-buffer-local 'chronometrist-details-range) (make-variable-buffer-local 'chronometrist-details-range)
#+END_SRC #+END_SRC
**** iso-date-p **** iso-date-p :function:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-iso-date-p (string) (defun chronometrist-iso-date-p (string)
(string-match-p (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) ;; (chronometrist-details-intervals-for-range '("2021-06-01" . "2021-06-03") chronometrist-events)
#+END_SRC #+END_SRC
**** set-range :command: **** set-range :command:writer:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-details-set-range () (defun chronometrist-details-set-range ()
"Prompt user for range for current `chronometrist-details' buffer." "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 (let ((input (completing-read-multiple
(concat "Range (blank, ISO-8601 date, " (concat "Range (blank, ISO-8601 date, "
"or two ISO-8601 dates/timestamps): ") "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 (pcase chronometrist-details-range
('nil nil) ('nil nil)
((pred stringp) ((pred stringp)
@ -3441,13 +3442,56 @@ intervals for which the predicate returns non-nil.")
(make-variable-buffer-local 'chronometrist-details-filter) (make-variable-buffer-local 'chronometrist-details-filter)
#+END_SRC #+END_SRC
**** set-filter :command: **** filter-match-p :function:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-details-set-filter (arg) (defun chronometrist-details-filter-match-p (plist filter)
"Prompt user for filter for current `chronometrist-details' buffer.") "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 #+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 ** Provide
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(provide 'chronometrist) (provide 'chronometrist)