cmc/cleberg.net

My personal web garden & blog.

clone: git clone https://gitbay.org/cmc/cleberg.net.git

2601873a0da273769420e48d23221ebf8b600fe2

signed_unknown_key

author: Christian Cleberg <hello@cleberg.net> · 2026-02-21T19:53:14Z
committer: <noreply@github.com>

add /garden section to site (#5)

* publish new post: automating-weblorg-deployments

* fix publish.el comments

* allow for case-insensitive env variable

* obfuscate port

* update run rules

* update run rules

* allow for case-insensitive env variable

* publish new post: automating-weblorg-deployments

* add /garden section to site
 content/garden/org-mode.org | 133 ++++++++++++++++++++++++++++++++++++++++++++
 publish.el                  |  17 ++++++
 theme/static/styles.css     |   2 +
 theme/templates/base.html   |   1 +
 theme/templates/garden.html |  18 ++++++
 5 files changed, 171 insertions(+)

diff --git a/content/garden/org-mode.org b/content/garden/org-mode.org
new file mode 100644
index 0000000..da6e087
--- /dev/null
+++ b/content/garden/org-mode.org
@@ -0,0 +1,133 @@
+#+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
diff --git a/publish.el b/publish.el
index 7f2b4be..cd3de56 100644
--- a/publish.el
+++ b/publish.el
@@ -52,6 +52,23 @@
  :output ".build/blog/index.html"
  :url "/blog/")
 
+;; Garden post route
+(weblorg-route
+ :name "garden"
+ :input-pattern "content/garden/*.org"
+ :template "post.html"
+ :output ".build/garden/{{ slug }}.html"
+ :url "/garden/{{ slug }}.html")
+
+;; Garden index page route
+(weblorg-route
+ :name "garden-index"
+ :input-pattern "content/garden/*.org"
+ :input-aggregate #'weblorg-input-aggregate-all-desc
+ :template "garden.html"
+ :output ".build/garden/index.html"
+ :url "/garden/")
+
 ;; Page post route
 (weblorg-route
  :name "pages"
diff --git a/theme/static/styles.css b/theme/static/styles.css
index 7029e9e..37c3e4f 100644
--- a/theme/static/styles.css
+++ b/theme/static/styles.css
@@ -135,6 +135,8 @@ img:hover { filter: grayscale(0); }
     gap: 0.5rem;
 }
 
+/* -- Garden -- */
+
 /* Style the footnote number to be a fixed width for alignment */
 .footnum {
     font-weight: bold;
diff --git a/theme/templates/base.html b/theme/templates/base.html
index 9f1c48e..3a2ff79 100644
--- a/theme/templates/base.html
+++ b/theme/templates/base.html
@@ -23,6 +23,7 @@
             <li><a href="/">Home</a></li>
             <li><a href="/about/">About</a></li>
             <li><a href="/blog/">Blog</a></li>
+            <li><a href="/garden/">Garden</a></li>
             <li><a href="/services/">Services</a></li>
         </ul>
     </nav>
diff --git a/theme/templates/garden.html b/theme/templates/garden.html
new file mode 100644
index 0000000..dda3884
--- /dev/null
+++ b/theme/templates/garden.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %} {% block subtitle %}Garden | {% endblock %} {% block main %}
+<h1>Garden</h1>
+<p>
+	Notes in various states of growth. Some are stubs, some are developed.
+	Browse around.
+</p>
+<br>
+<ul class="garden-index">
+{% for post in posts %}
+<li>
+	<a href='{{ url_for("garden", slug=post.slug) }}'>{{ post.title }}</a>
+	{% if post.description %}
+	<span class="garden-desc">{{ post.description }}</span>
+	{% endif %}
+</li>
+{% endfor %}
+</ul>
+{% endblock %}