🌱 org-mode
· Tidbits about the best plaintext language: org-mode.
Table of Contents
1. 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:
#+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: -- => 3:00
:END:
* Project 2
:LOGBOOK:
CLOCK: -- => 6:00
:END:
The elisp function to generate the report:
;; 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)
The output file:
#+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 |