cmc/dotfiles

Using GNU Stow to manage my dotfiles.

clone: git clone https://gitbay.org/cmc/dotfiles.git

76e87ab090cecb92579645ce3a5bb0247ef548b1

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-02-21T19:15:48Z

move time reporting to its own internal function
 common/doom/.config/doom/config.el | 85 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 85 insertions(+)

diff --git a/common/doom/.config/doom/config.el b/common/doom/.config/doom/config.el
index fb216eb..2a4cd96 100644
--- a/common/doom/.config/doom/config.el
+++ b/common/doom/.config/doom/config.el
@@ -47,3 +47,88 @@
     ("xnow" "" (lambda () (insert (format-time-string "%H:%M:%S"))))
     ))
 
+;; Time Report
+(defun tr/parse-org-clock-entries (file)
+  "Return list of (date heading minutes) from FILE's CLOCK entries."
+  (let (entries)
+    (with-current-buffer (find-file-noselect file)
+      (org-with-wide-buffer
+       (goto-char (point-min))
+       (while (re-search-forward
+               "CLOCK: \\[\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) [A-Za-z]\\{3\\} [0-9:]\\{5\\}\\]--\\[\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) [A-Za-z]\\{3\\} \\([0-9:]\\{5\\}\\)\\] =>  *\\([0-9]+\\):\\([0-9]+\\)"
+               nil t)
+         (let* ((date (match-string 1))
+                (h    (string-to-number (match-string 4)))
+                (m    (string-to-number (match-string 5)))
+                (mins (+ (* h 60) m))
+                (heading (save-excursion
+                           (org-back-to-heading t)
+                           (org-get-heading t t t t))))
+           (push (list date heading mins) entries)))))
+    (nreverse entries)))
+
+(defun tr/format-minutes (mins)
+  (format "%d:%02d" (/ mins 60) (mod mins 60)))
+
+(defun tr/generate-report (input-file output-file &optional tstart tend)
+  "Parse INPUT-FILE and write time report to OUTPUT-FILE."
+  (let* ((entries (tr/parse-org-clock-entries input-file))
+         (entries (if (or tstart tend)
+                      (cl-remove-if-not
+                       (lambda (e)
+                         (let ((d (car e)))
+                           (and (or (null tstart) (not (string< d tstart)))
+                                (or (null tend)   (not (string> d tend))))))
+                       entries)
+                    entries))
+         (totals (make-hash-table :test #'equal)))
+    (dolist (e entries)
+      (let ((key (cons (nth 0 e) (nth 1 e))))
+        (puthash key (+ (gethash key totals 0) (nth 2 e)) totals)))
+    (let* ((keys (hash-table-keys totals))
+           (keys (sort keys (lambda (a b)
+                              (or (string< (car a) (car b))
+                                  (and (string= (car a) (car b))
+                                       (string< (cdr a) (cdr b)))))))
+           (current-week nil))
+      (with-temp-file output-file
+        (insert "#+title: Time Report\n\n")
+        (insert (format "| %-12s | %-45s | %5s |\n" "Date" "Code" "Hours"))
+        (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n"))
+        (let (week-mins week-start)
+          (dolist (k keys)
+            (let* ((date (car k))
+                   (heading (cdr k))
+                   (mins (gethash k totals))
+                   (time (date-to-time (concat date " 00:00:00")))
+                   (week (format-time-string "%Y-W%V" time)))
+              (when (and week-start (not (string= week current-week)))
+                (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n"))
+                (insert (format "| %-12s | %-45s | %5s |\n" current-week "WEEK TOTAL" (tr/format-minutes week-mins)))
+                (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n"))
+                (setq week-mins 0))
+              (unless (string= week current-week)
+                (setq current-week week week-start date week-mins 0))
+              (setq week-mins (+ week-mins mins))
+              (insert (format "| %-12s | %-45s | %5s |\n" date heading (tr/format-minutes mins)))))
+          (when week-mins
+            (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n"))
+            (insert (format "| %-12s | %-45s | %5s |\n" current-week "WEEK TOTAL" (tr/format-minutes week-mins)))))
+        (insert "\n")))))
+
+(defun tr/time-report-interactive ()
+  "Interactively generate a time report from an org file."
+  (interactive)
+  (let* ((input  (read-file-name "Time tracking file: " nil nil t))
+         (output (read-file-name "Output report file: "))
+         (tstart (read-string "Start date (YYYY-MM-DD, blank for none): "))
+         (tend   (read-string "End date   (YYYY-MM-DD, blank for none): "))
+         (tstart (if (string-empty-p tstart) nil tstart))
+         (tend   (if (string-empty-p tend)   nil tend)))
+    (tr/generate-report input output tstart tend)
+    (find-file output)
+    (message "Report written to %s" output)))
+
+;; Map time report to keybindings
+(map! :leader
+      :desc "Generate time report" "z t" #'tr/time-report-interactive)