;;; org-log.el --- Quantitative logging for Org headings -*- lexical-binding: t; -*- ;; Copyright (C) 2019 Free Software Foundation, Inc. ;; Author: Eric Abrahamsen ;; Maintainer: Eric Abrahamsen ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; This library provides hooks and functions for quantitative logging ;; in Org: meaning that storing a log note may prompt for one or more ;; quantitative values, and store them as part of the note. Those ;; values can later be viewed as tables. Essentially this is a ;; generalization of org-clock, to allow logging of other values. ;; Like org-clock, it also includes a command to view logged data as ;; an Org table. ;; In terms of format, a logged value appears as an all-caps label, ;; followed by a colon, followed by the data itself, followed by a ;; semi-colon. For instance: ;; BP: 120/60; PULSE: 45; ;; The `org-log-buffer-setup-hook' is used to prompt the user for log ;; values to store. It does this by looking for a LOG_VALUES property ;; on the heading, which should be a space-separated list of all-caps ;; value labels. These labels can also contain information about the ;; units used for the value. Units are specified immediately after ;; the value label in square brackets, like so: ;; DISTANCE[mi] ;; This unit information will be automatically incorporated into ;; tables created to view logged data. Call the ;; `calc-view-units-table' command to see all valid units; you can ;; define new units using `calc-define-unit', which see. ;;; Code: (require 'org) (require 'calc-units) (defun org-log-prompt () "Prompt the user for log values. Insert values into the current log note." ;; This is called in the log buffer. Only fire for state and note; ;; later add clock-out. (when (memq org-log-note-purpose '(state note)) (let ((values (with-current-buffer (marker-buffer org-log-note-marker) (save-excursion (goto-char org-log-note-marker) (org-entry-get (point) "LOG_VALUES" 'selective))))) (when (and (stringp values) (null (string-empty-p values))) (unless (bolp) ; This might follow a clock line. (insert "; ")) (dolist (val (split-string values)) ;; Maybe strip off units. (setq val (substring val 0 (string-match-p "\\[" val))) (insert val ": ") (insert (read-string (format "%s: " val)) "; ")))))) (defun org-log--collect-data (id) "Collect log data from heading with id ID. When valid data is found, it is returned as a list of lists. Each sublist starts with the timestamp of the log entry, followed by data keys and values, in the order they were found in the log entry. If no valid data is found, return nil." (save-excursion (org-id-goto id) (goto-char (org-log-beginning)) (let* ((struct (org-list-struct)) (labels (org-entry-get (point) "LOG_VALUES" 'selective)) (entries (when (and (stringp labels) (null (string-empty-p labels))) ;; First element is a list of value labels. (list (cons "TIMESTAMP" (mapcar (lambda (str) (substring str 0 (string-match-p "\\[" str))) (split-string labels)))))) elt data) (when (and entries struct) ;; Get onto the list item. (forward-char) (while (equal 'item (org-element-type (setq elt (org-element-at-point)))) ;; Move past state/timestamp line. (forward-line) (while (re-search-forward "[[:upper:]]+: \\([^;]+\\)" (point-at-eol) t) (push (match-string-no-properties 1) data)) (when data (save-excursion (forward-line -1) ;; Get the log entry timestamp. (setq data (cons (if (re-search-forward org-ts-regexp-inactive (point-at-eol) t) (match-string-no-properties 0) "none") (nreverse data)))) (push data entries) (setq data nil)) (goto-char (org-element-property :end elt))) (nreverse entries))))) (defun org-log-create-dblock () "Create a dblock with a table for this heading's log data." (interactive) (let ((id (org-id-get-create))) ;; Anyway, this call is the heart of it. (org-create-dblock `(:name "log" :id ,id)) (org-update-dblock))) (defun org-dblock-write:log (params) "Write the log dblock table." (let ((data (org-log--collect-data (plist-get params :id)))) (when data (save-excursion (insert (format "|%s|\n" (mapconcat #'capitalize (pop data) "|")) "|-|\n" (mapconcat (lambda (row) (format "|%s|" (mapconcat #'identity row "|"))) data "\n"))) (org-table-align)))) (add-hook 'org-log-buffer-setup-hook #'org-log-prompt) (provide 'org-log) ;;; org-log.el ends here