4

Let's say I have the following checklist in org-mode:

* TODO Example [/]
- [ ] First
- [ ] Second

How do I automatically add a timestamped entry into the logbook drawer when an item is checked? Ideally, I would like to have something like this:

* TODO Example [1/2]
:LOGBOOK:
- "First" was checked [2023-01-01 Sun 17:00]
:END:
- [X] First
- [ ] Second

I took a look at the checkboxes docs, but I didn't see any logging options there.

2 Answers 2

3

The following code should achieve what you want:

(defun org-log-checklist-item (item)
"Insert clocked item into logbook drawer.
Create drawer if it does not exist yet."
  (save-excursion
    (org-previous-visible-heading 1)
    (while (not (= (org-current-level) 1))
      (org-previous-visible-heading 1))
    (forward-line)
    (let* ((element (org-element-at-point))
           (logbookp (string= (org-element-property :drawer-name element)
                              "LOGBOOK")))
      (if logbookp
          (goto-char (org-element-property :contents-end element))
        (org-insert-drawer nil "LOGBOOK"))
      (insert "- \"" item "\" was checked ")
      (org-insert-time-stamp (current-time) t t)
      (when logbookp
        (insert "\n")))))

(defun org-checkbox-item ()
"Retrieve the contents (text) of the item."
  (save-excursion
    (beginning-of-line)
    (search-forward "]")
    (forward-char)
    (buffer-substring-no-properties (point) (line-end-position))))

(defun org-checklist-change-advice-function (&rest _)
  (when (org-at-item-checkbox-p)
    (let ((checkedp (save-excursion
                      (beginning-of-line)
                      (search-forward "[")
                      (looking-at-p "X"))))
      (when checkedp
        (org-log-checklist-item (org-checkbox-item))))))

(advice-add 'org-list-struct-apply-struct :after #'org-checklist-change-advice-function)
;; (advice-add 'org-toggle-checkbox :after #'org-checklist-change-advice-function)

The uncommented advice-add works for toggling with C-c C-c, the commented advice-add works when toggling with org-toggle-checkbox (C-c C-c does not use org-toggle-checkbox, I don't know why the same/similar functionality has been implemented twice).

As the code, otherwise, is more or less self-explanatory, I don't add any extra explanation. If something is unclear, then read the docstrings of the functions and/or use edebug, which is a very easy and powerful tool for analyzing the code.

Also, I am not sure about the robustness of the code (it is just a most simplistic and straightforward implementation), anyway it should get you on the way at least.

5
  • The problem is that the usual way to toggle (C-c C-c) does not go through org-toggle-checkbox. Arguably, that's a bug: the org-ctrl-c-ctrl-c code looks ugly and ad-hoc but that's what it is.
    – NickD
    Commented Jan 18, 2023 at 16:22
  • @NickD Thanks, I don't much use this functionality myself, and clearly wasn't aware of this 'redundancy'. Anyway, I have adapted the answer to work also for C-c C-c. Commented Jan 18, 2023 at 20:10
  • So adding the advice to both functions should catch all possibilities (except manually changing the checkbox :-) ), correct?
    – NickD
    Commented Jan 18, 2023 at 20:15
  • Correct, but maybe it is nice to keep one, for toggling without clocking. So the user now can decide for himself (by commenting in out the preferred advice). Commented Jan 18, 2023 at 20:17
  • 1
    Looks good, thanks!
    – NickD
    Commented Jan 18, 2023 at 20:39
1

Thanks a lot for this, I see it is veeery nice to have a tracking of the checkbox checking (why is not built-in in orgmode?!). It looks similar to what other ticketing system do with reporting when a checkbox was checked

I changed @dalanicolai code proposal

Relevant changes:

  • It prints the logbook in reverse order (on top the newest), with var org-log-states-order-reversed which is the default option (yea, we could do taking in account this var to change from :contents-begin to :contents-end, I also think it should be other APIs that could simplify all of this handling, but no idea)
  • It tries not to generate a logbook when it does not exist (I could not make it work). It is strange to happen, because you can configure to report on most of the TODO state changes. when it cannot make it, warns on minibuffer
  • When succeeds, shows the same message in minibuffer and on logbook
  • Bugfix: if CLOSED, SCHEDULED or DEADLINE are present after the header, it does not try to recreate the logbook. I know how to detect that situation, but not how to insert logbook when that is the situation. Maybe with that code you can do your version that includes it :)
;; not working when logbook (TRACE) is not present; it is very difficult to fix it
(defun org-log-checklist-item (item)
  "Insert clocked item into logbook drawer."
  (save-excursion
    ;; Move to the top of the entry
    (org-back-to-heading t)
    ;; Look for the logbook drawer
    ;; Determine the end boundary of the current heading in the search
    (let ((element-end (save-excursion (org-end-of-subtree))))
      ;; Search for the drawer within the current heading only
      (if (re-search-forward org-log-state-notes-into-drawer element-end t)
          (let* ((element (org-element-at-point))
                 (logbookp (string= (org-element-property :drawer-name element)
                                    org-log-state-notes-into-drawer))
                 (event-msg (concat "- Checkbox \"" item "\" was checked ")))
            (if logbookp
                (goto-char (org-element-property :contents-begin element)))
            (insert event-msg)
            (message event-msg)
            (org-insert-time-stamp (current-time) t t)
            (when logbookp
              (insert "\n")))
        ;; If no logbook drawer found, print an error
        (error "Logbook drawer not found.")))))

;; warning: fails silently if no text in the checkbox
(defun org-checkbox-item ()
"Retrieve the contents (text) of the item."
  (save-excursion
    (beginning-of-line)
    (search-forward "]")
    (forward-char)
    (buffer-substring-no-properties (point) (line-end-position))))

(defun org-checklist-change-advice-function (&rest _)
  (when (org-at-item-checkbox-p)
    (let ((checkedp (save-excursion
                      (beginning-of-line)
                      (search-forward "[")
                      (looking-at-p "X"))))
      (when checkedp
        (org-log-checklist-item (org-checkbox-item))))))

(advice-add 'org-list-struct-apply-struct :after #'org-checklist-change-advice-function)
;; (advice-add 'org-toggle-checkbox :after #'org-checklist-change-advice-function)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.