#+title: 🌱 org-mode #+slug: org-mode #+description: Tidbits about the best plaintext language: org-mode. #+date: [2026-02-21 Sat 12:39:27] * Time Reporting If you maintain a log of time clocks, you can create a weekly report that will break down time per heading, per day, per week. The time tracker file: #+begin_src org ,#+title: Time Tracker ,#+startup: showall # To generate a time report: # 1. Open Emacs with this file # 2. SPC z t (or M-x tr/time-report-interactive) # 3. Enter this file as input, a path for output, and optional date range (YYYY-MM-DD) ,* Project 1 :LOGBOOK: CLOCK: [2026-02-19 Thu 09:00]--[2026-02-19 Thu 12:00] => 3:00 :END: ,* Project 2 :LOGBOOK: CLOCK: [2026-02-20 Fri 10:00]--[2026-02-20 Fri 16:00] => 6:00 :END: #+end_src The elisp function to generate the report: #+begin_src elisp ;; 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) #+end_src The output file: #+begin_src org ,#+title: Time Report | Date | Code | Hours | |------------+------------+-------| | 2026-02-19 | Project 1 | 3:00 | | 2026-02-20 | Project 2 | 6:00 | |------------+------------+-------| | 2026-W08 | WEEK TOTAL | 9:00 | #+end_src