Commit 76e87ab090
Verified · cmc
Layout: unified · split
common/doom/.config/doom/config.el +85
| @@ -47,3 +47,88 @@ | ||
| 47 | 47 | ("xnow" "" (lambda () (insert (format-time-string "%H:%M:%S")))) |
| 48 | 48 | )) |
| 49 | 49 | |
| 50 | ;; Time Report | |
| 51 | (defun tr/parse-org-clock-entries (file) | |
| 52 | "Return list of (date heading minutes) from FILE's CLOCK entries." | |
| 53 | (let (entries) | |
| 54 | (with-current-buffer (find-file-noselect file) | |
| 55 | (org-with-wide-buffer | |
| 56 | (goto-char (point-min)) | |
| 57 | (while (re-search-forward | |
| 58 | "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]+\\)" | |
| 59 | nil t) | |
| 60 | (let* ((date (match-string 1)) | |
| 61 | (h (string-to-number (match-string 4))) | |
| 62 | (m (string-to-number (match-string 5))) | |
| 63 | (mins (+ (* h 60) m)) | |
| 64 | (heading (save-excursion | |
| 65 | (org-back-to-heading t) | |
| 66 | (org-get-heading t t t t)))) | |
| 67 | (push (list date heading mins) entries))))) | |
| 68 | (nreverse entries))) | |
| 69 | ||
| 70 | (defun tr/format-minutes (mins) | |
| 71 | (format "%d:%02d" (/ mins 60) (mod mins 60))) | |
| 72 | ||
| 73 | (defun tr/generate-report (input-file output-file &optional tstart tend) | |
| 74 | "Parse INPUT-FILE and write time report to OUTPUT-FILE." | |
| 75 | (let* ((entries (tr/parse-org-clock-entries input-file)) | |
| 76 | (entries (if (or tstart tend) | |
| 77 | (cl-remove-if-not | |
| 78 | (lambda (e) | |
| 79 | (let ((d (car e))) | |
| 80 | (and (or (null tstart) (not (string< d tstart))) | |
| 81 | (or (null tend) (not (string> d tend)))))) | |
| 82 | entries) | |
| 83 | entries)) | |
| 84 | (totals (make-hash-table :test #'equal))) | |
| 85 | (dolist (e entries) | |
| 86 | (let ((key (cons (nth 0 e) (nth 1 e)))) | |
| 87 | (puthash key (+ (gethash key totals 0) (nth 2 e)) totals))) | |
| 88 | (let* ((keys (hash-table-keys totals)) | |
| 89 | (keys (sort keys (lambda (a b) | |
| 90 | (or (string< (car a) (car b)) | |
| 91 | (and (string= (car a) (car b)) | |
| 92 | (string< (cdr a) (cdr b))))))) | |
| 93 | (current-week nil)) | |
| 94 | (with-temp-file output-file | |
| 95 | (insert "#+title: Time Report\n\n") | |
| 96 | (insert (format "| %-12s | %-45s | %5s |\n" "Date" "Code" "Hours")) | |
| 97 | (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n")) | |
| 98 | (let (week-mins week-start) | |
| 99 | (dolist (k keys) | |
| 100 | (let* ((date (car k)) | |
| 101 | (heading (cdr k)) | |
| 102 | (mins (gethash k totals)) | |
| 103 | (time (date-to-time (concat date " 00:00:00"))) | |
| 104 | (week (format-time-string "%Y-W%V" time))) | |
| 105 | (when (and week-start (not (string= week current-week))) | |
| 106 | (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n")) | |
| 107 | (insert (format "| %-12s | %-45s | %5s |\n" current-week "WEEK TOTAL" (tr/format-minutes week-mins))) | |
| 108 | (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n")) | |
| 109 | (setq week-mins 0)) | |
| 110 | (unless (string= week current-week) | |
| 111 | (setq current-week week week-start date week-mins 0)) | |
| 112 | (setq week-mins (+ week-mins mins)) | |
| 113 | (insert (format "| %-12s | %-45s | %5s |\n" date heading (tr/format-minutes mins))))) | |
| 114 | (when week-mins | |
| 115 | (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n")) | |
| 116 | (insert (format "| %-12s | %-45s | %5s |\n" current-week "WEEK TOTAL" (tr/format-minutes week-mins))))) | |
| 117 | (insert "\n"))))) | |
| 118 | ||
| 119 | (defun tr/time-report-interactive () | |
| 120 | "Interactively generate a time report from an org file." | |
| 121 | (interactive) | |
| 122 | (let* ((input (read-file-name "Time tracking file: " nil nil t)) | |
| 123 | (output (read-file-name "Output report file: ")) | |
| 124 | (tstart (read-string "Start date (YYYY-MM-DD, blank for none): ")) | |
| 125 | (tend (read-string "End date (YYYY-MM-DD, blank for none): ")) | |
| 126 | (tstart (if (string-empty-p tstart) nil tstart)) | |
| 127 | (tend (if (string-empty-p tend) nil tend))) | |
| 128 | (tr/generate-report input output tstart tend) | |
| 129 | (find-file output) | |
| 130 | (message "Report written to %s" output))) | |
| 131 | ||
| 132 | ;; Map time report to keybindings | |
| 133 | (map! :leader | |
| 134 | :desc "Generate time report" "z t" #'tr/time-report-interactive) | |