org-clock-agg: extra-params

This commit is contained in:
Pavel Korytov 2023-12-10 01:05:53 +03:00
parent 7e7f6ffc10
commit 8f2ce1bea2

View file

@ -87,8 +87,23 @@ See `format-time-string' for the list of available format specifiers."
:group 'org-clock-agg) :group 'org-clock-agg)
(defcustom org-clock-agg-properties nil (defcustom org-clock-agg-properties nil
"Org properties to include in `org-clock-agg' views." "Org properties to include in `org-clock-agg' views.
Either set this interactively or reset the `org-ql-cache' variable
manually after setting."
:type '(repeat string) :type '(repeat string)
:group 'org-clock-agg
:set (lambda (&rest _)
(setq org-ql-cache (make-hash-table :weakness 'key))))
(defconst org-clock-agg--extra-params-default
'(("Show elements:" . (checkbox :extras-key :show-elems))
("Add \"Ungrouped\"" . (checkbox :extras-key :add-ungrouped)))
"Default set of extra parameters for `org-clock-agg' views.")
(defcustom org-clock-agg-extra-params nil
"Extra parameters for `org-clock-agg' views."
:type '(alist :key-type string :value-type sexp)
:group 'org-clock-agg) :group 'org-clock-agg)
(defface org-clock-agg-group-face (defface org-clock-agg-group-face
@ -348,8 +363,10 @@ ELEMS is a list as descbribed in `org-clock-agg--parse-headline'."
(cl-defmacro org-clock-agg-defgroupby (name &body body) (cl-defmacro org-clock-agg-defgroupby (name &body body)
"Define a grouping function for `org-clock-agg'. "Define a grouping function for `org-clock-agg'.
NAME is the name of the function. BODY has a variable `elem' bound, NAME is the name of the function. BODY has the following variables bound:
which is an alist as described in `org-clock-agg--parse-headline'. - `elem' - an alist as described in `org-clock-agg--parse-headline'
- `extra-params' - and alist with extra parameters. See
`org-clock-agg' on that.
The function must return a list of strings, which are the group The function must return a list of strings, which are the group
names. names.
@ -369,7 +386,7 @@ sort function."
(unless readable-name (unless readable-name
(setq readable-name (symbol-name name))) (setq readable-name (symbol-name name)))
`(progn `(progn
(defun ,func-name (elem) (defun ,func-name (elem extra-params)
,@body) ,@body)
(setf (alist-get ',name org-clock-agg-groupby-functions) (setf (alist-get ',name org-clock-agg-groupby-functions)
'((:function . ,func-name) '((:function . ,func-name)
@ -573,7 +590,41 @@ return value."
(cons elem (alist-get :elems prev-val)))))))) (cons elem (alist-get :elems prev-val))))))))
alist) alist)
(defun org-clock-agg--groupby (elems groupby-list sort-list sort-order-list) (defun org-clock-agg--add-ungrouped (tree)
"Add \"Ungrouped\" nodes to TREE.
Such node is added to every node in TREE that has both `:elems',
i.e. ungrouped elements, and `:children'. This can happen when only
part of elements of the node was grouped by a grouping function.
Adding the \"Ungrouped\" node with all the ungrouped elements ensures that
the total time spent in node equals the sum of the total time spent in
its children.
TREE is a tree as returned by `org-clock-agg--groupby'."
(dolist (node tree)
(let ((children (alist-get :children (cdr node)))
(elems (alist-get :elems (cdr node))))
(org-clock-agg--add-ungrouped children)
(when (and children elems)
(let ((total (seq-reduce (lambda (acc val)
(+ acc (alist-get :duration val)))
elems 0)))
(setf (alist-get :children (cdr node))
(cons
`("Ungrouped"
(:total . ,total)
(:groupby . ((:readable-name . "Ungrouped")))
(:children)
(:sort-symbol . total)
(:sort-order)
(:elems . ,elems))
(alist-get :children (cdr node)))))
(setf (alist-get :elems (cdr node)) nil))))
tree)
(defun org-clock-agg--groupby (elems groupby-list sort-list sort-order-list extra-params)
"Group ELEMS for `org-clock-agg' into a tree. "Group ELEMS for `org-clock-agg' into a tree.
ELEMS is a list as described in `org-clock-agg--parse-headline'. ELEMS is a list as described in `org-clock-agg--parse-headline'.
@ -585,6 +636,10 @@ in SORT-LIST.
The root group is always added to the beginning of GROUPBY-LIST. The root group is always added to the beginning of GROUPBY-LIST.
EXTRA-PARAMS is an alist of extra parameters for the grouping
functions. If `:add-ungrouped' is non-nil, add \"Ungrouped\" nodes to
the tree. See `org-clock-agg' for more.
The return value is a tree of alists with the following keys: The return value is a tree of alists with the following keys:
- `:total' - total seconds spent in group - `:total' - total seconds spent in group
- `:groupby' - grouping function (as in the variable - `:groupby' - grouping function (as in the variable
@ -594,7 +649,7 @@ The return value is a tree of alists with the following keys:
sorting sorting
- `:sort-order' - if non-nil, reverse the sort order - `:sort-order' - if non-nil, reverse the sort order
- `:elems' - list of elements in the group, same form as ELEMS." - `:elems' - list of elements in the group, same form as ELEMS."
(let (res) (let (tree)
(dolist (elem elems) (dolist (elem elems)
(let* ((group-symbols (cons 'root-group groupby-list)) (let* ((group-symbols (cons 'root-group groupby-list))
(sort-symbols (cons 'total sort-list)) (sort-symbols (cons 'total sort-list))
@ -604,14 +659,16 @@ The return value is a tree of alists with the following keys:
for sort-symbol in sort-symbols for sort-symbol in sort-symbols
for sort-order in sort-orders for sort-order in sort-orders
for groupby = (alist-get group-symbol org-clock-agg-groupby-functions) for groupby = (alist-get group-symbol org-clock-agg-groupby-functions)
for group-values = (funcall (alist-get :function groupby) elem) for group-values = (funcall (alist-get :function groupby) elem extra-params)
append append
(mapcar (mapcar
(lambda (group-value) (lambda (group-value)
(list group-value groupby sort-symbol sort-order)) (list group-value groupby sort-symbol sort-order))
group-values)))) group-values))))
(setq res (org-clock-agg--groupby-apply res groups elem)))) (setq tree (org-clock-agg--groupby-apply tree groups elem))))
res)) (when (alist-get :add-ungrouped extra-params)
(setq tree (org-clock-agg--add-ungrouped tree)))
tree))
(defun org-clock-agg--ungroup (tree) (defun org-clock-agg--ungroup (tree)
"Reverse grouping for TREE. "Reverse grouping for TREE.
@ -806,15 +863,22 @@ TREE is a tree of alists as described in `org-clock-agg--groupby'."
org-clock-agg-sort-functions)) org-clock-agg-sort-functions))
(toggle :on "Reverse order" :off "Normal order")))) (toggle :on "Reverse order" :off "Normal order"))))
(defun org-clock-agg--render-switches () (defun org-clock-agg--extras-notify (widget &rest _)
"Render switches for the `org-clock-agg' buffer." "Notify funciton for extra-params widgets.
(insert (propertize "Show elements: " 'face 'widget-button))
(widget-create 'checkbox WIDGET is the instance of the widget that was changed."
:notify (lambda (widget &rest ignore) (let ((extras-key (widget-get widget :extras-key)))
(setf (alist-get :show-elems org-clock-agg--params) (setf (alist-get extras-key
(widget-value widget))) (alist-get :extra-params org-clock-agg--params))
nil) (widget-value widget))))
(insert "\n"))
(defun org-clock-agg--render-extra-params ()
"Render extra-params for the `org-clock-agg' buffer."
(pcase-dolist (`(,name . ,params) (append org-clock-agg--extra-params-default
org-clock-agg-extra-params))
(insert (propertize name 'face 'widget-button) " ")
(apply #'widget-create `(,@params :notify org-clock-agg--extras-notify))
(insert "\n")))
(defun org-clock-agg--render-controls () (defun org-clock-agg--render-controls ()
"Render controls for the `org-clock-agg' buffer." "Render controls for the `org-clock-agg' buffer."
@ -826,7 +890,7 @@ TREE is a tree of alists as described in `org-clock-agg--groupby'."
(insert "\n\n") (insert "\n\n")
(org-clock-agg--render-controls-groupby) (org-clock-agg--render-controls-groupby)
(insert "\n") (insert "\n")
(org-clock-agg--render-switches) (org-clock-agg--render-extra-params)
(insert "\n") (insert "\n")
(widget-create 'push-button (widget-create 'push-button
:notify (lambda (&rest ignore) :notify (lambda (&rest ignore)
@ -945,25 +1009,25 @@ created by `org-clock-agg--render-controls-files'."
(alist-get files org-clock-agg-files-preset nil nil #'equal)) (alist-get files org-clock-agg-files-preset nil nil #'equal))
(t files))) (t files)))
(cl-defun org-clock-agg-exec (from to files groupby sort sort-order) (cl-defun org-clock-agg-exec (from to files groupby sort sort-order extra-params)
"Aggregate org-clock data and return the result as tree. "Aggregate org-clock data and return the result as tree.
See `org-clock-agg' for the meaning of FROM, TO, FILES, GROUPBY, See `org-clock-agg' for the meaning of FROM, TO, FILES, GROUPBY, SORT,
SORT, and SORT-ORDER. See `org-clock-agg--groupby' for the SORT-ORDER, and EXTRA-PARAMS. See `org-clock-agg--groupby' for the
return value description." return value description."
(let* ((files (org-clock-agg--parse-files files)) (let* ((files (org-clock-agg--parse-files files))
(elems (org-clock-agg--query from to files)) (elems (org-clock-agg--query from to files))
(tree (org-clock-agg--groupby elems groupby sort sort-order)) (tree (org-clock-agg--groupby elems groupby sort sort-order extra-params))
(tree (org-clock-agg--groupby-sort tree))) (tree (org-clock-agg--groupby-sort tree)))
(cons elems tree))) (cons elems tree)))
(defun org-clock-agg-refresh () (defun org-clock-agg-refresh ()
"Refresh the `org-clock-agg' buffer." "Refresh the `org-clock-agg' buffer."
(interactive) (interactive)
(cl-destructuring-bind (&key from to files groupby sort sort-order show-elems) (cl-destructuring-bind (&key from to files groupby sort sort-order extra-params)
(org-clock-agg--alist-to-plist org-clock-agg--params) (org-clock-agg--alist-to-plist org-clock-agg--params)
(pcase-let ((`(,elems . ,tree) (pcase-let ((`(,elems . ,tree)
(org-clock-agg-exec from to files groupby sort sort-order))) (org-clock-agg-exec from to files groupby sort sort-order extra-params)))
(setq-local org-clock-agg--elems elems) (setq-local org-clock-agg--elems elems)
(setq-local org-clock-agg--tree tree) (setq-local org-clock-agg--tree tree)
(save-excursion (save-excursion
@ -973,7 +1037,9 @@ return value description."
(beginning-of-line) (beginning-of-line)
(delete-region (point) (point-max)) (delete-region (point) (point-max))
(dolist (node tree) (dolist (node tree)
(org-clock-agg--render-tree-node node show-elems))))))) (org-clock-agg--render-tree-node
node
(alist-get :show-elems extra-params))))))))
(defun org-clock-agg-generate-report () (defun org-clock-agg-generate-report ()
"Generate a report function from the `org-clock-agg' state." "Generate a report function from the `org-clock-agg' state."
@ -994,7 +1060,7 @@ return value description."
'(,from ,to ,files ,groupby ,sort ,sort-order ,show-elems))))))) '(,from ,to ,files ,groupby ,sort ,sort-order ,show-elems)))))))
(switch-to-buffer buffer))) (switch-to-buffer buffer)))
(defun org-clock-agg (from to files groupby sort sort-order show-elems) (defun org-clock-agg (from to files groupby sort sort-order extra-params)
"Aggregate org-clock data. "Aggregate org-clock data.
The function creates an interactive buffer to configure the The function creates an interactive buffer to configure the
@ -1020,11 +1086,21 @@ defines the sort logic for the results of the Nth GROUPBY function.
SORT-ORDER has to be the same length as SORT. If Nth entry is non-nil, SORT-ORDER has to be the same length as SORT. If Nth entry is non-nil,
the sorting is reversed. the sorting is reversed.
EXTRA-PARAMS is an alist of \"extra parameters\". Possible keys are
defined by `org-clock-agg--extra-params-default' and
`org-clock-agg-extra-params'. The built-in parameters are:
- `:show-elems' - whether to show raw elements for each group in the
buffer. An \"element\" is one org-clock record.
- `:add-ungrouped' - whether to add the \"Ungrouped\" group to the
results.
`org-clock-agg-extra-params' can be used to define new parameters.
This is meant to be used by custom aggregation functions to control
their behavior in runtime.
See the mentioned variables for and the interactive buffer for the See the mentioned variables for and the interactive buffer for the
available group and sort functions; use `org-clock-agg-defgroupby' and available group and sort functions; use `org-clock-agg-defgroupby' and
`org-clock-agg-defsort' to define new ones. `org-clock-agg-defsort' to define new ones."
If SHOW-ELEMS is non-nil, the individual elements are shown as well."
(interactive (list -7 0 'org-agenda nil nil nil nil)) (interactive (list -7 0 'org-agenda nil nil nil nil))
(let* ((buffer (generate-new-buffer "*org-clock-agg*"))) (let* ((buffer (generate-new-buffer "*org-clock-agg*")))
(switch-to-buffer-other-window buffer) (switch-to-buffer-other-window buffer)
@ -1037,7 +1113,7 @@ If SHOW-ELEMS is non-nil, the individual elements are shown as well."
(:groupby . ,groupby) (:groupby . ,groupby)
(:sort . ,sort) (:sort . ,sort)
(:sort-order . ,sort-order) (:sort-order . ,sort-order)
(:show-elems . ,show-elems))) (:extra-params . ,extra-params)))
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(org-clock-agg--render-controls) (org-clock-agg--render-controls)
(org-clock-agg-refresh)) (org-clock-agg-refresh))