Commit 76e87ab090
Verified · cmc
Layout: unified · split
common/doom/.config/doom/config.el +85
| @@ -47,3 +47,88 @@ | |||
| 47 | ("xnow" "" (lambda () (insert (format-time-string "%H:%M:%S")))) | 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) | ||