分享一个小函数:优雅地补录“过去完成”的 Org Todo 事项 (指定非当前的时刻)

大家在用 Org Mode 的时候应该遇到过这种场景:

一个带 repeater 的 TODO 任务(比如 +1w),实际上三天前就做完了,但当时没来得及记。如果你现在直接 C-c C-t 标记 DONE,Org 会用当前时刻作为 completion timestamp,导致下一次的 DEADLINE 也是基于今天往后推一周,而不是基于实际完成时间。

以前我都得手动去 Logbook 里改时间,再手动算下一次 Deadline,挺麻烦的。

所以写了个 advice 包装一下 org-todo。思路就是通过 cl-letf 把 Org 内部获取时间的函数(org-current-effective-time 等)临时 shadow 掉,让它认为“现在”就是我们指定的时间。

用法很简单:

  1. M-x my/set-next-org-todo-time,选一个时间(支持 org-read-date 的各种骚操作,比如 -3)。
  2. 接着按 C-c C-t 切换状态。
  3. 完事。Logbook 时间和 repeater 计算都会基于你刚才选的时间。

这个 advice 是一次性的(unwind-protect 里自毁),不会影响后续操作。

代码如下:

(defun my/set-next-org-todo-time (time-string)
  "Set the timestamp for the next `org-todo' invocation.
Normally, `org-todo' uses the current time. This function uses an
advice to temporarily shadow time-related functions like
`org-current-effective-time' so that the next `org-todo' call
operates relative to the provided timestamp."
  (interactive
   (list (org-read-date nil nil nil "Time for next org-todo: ")))
  (let* ((time (cond
                ((stringp time-string)
                 (org-time-string-to-time time-string))
                ((and (consp time-string)
                      (numberp (car time-string)))
                 time-string)
                (t
                 (current-time))))
         (advice
          (lambda (orig-fn &rest args)
            (cl-letf (((symbol-function 'org-current-effective-time)
                       (lambda (&optional _ignored) time))
                      ((symbol-function 'org-today)
                       (lambda ()
                         (time-to-days time)))
                      ((symbol-function 'org-timestamp-to-now)
                       (lambda (timestamp-string &optional seconds)
                         (let ((fdiff (if seconds #'float-time #'time-to-days)))
                           (- (funcall fdiff (org-time-string-to-time timestamp-string))
                              (funcall fdiff time))))))
              (unwind-protect
                  (apply orig-fn args)
                (advice-remove 'org-todo 'override-todo-timestamp-once))))))
    (advice-add 'org-todo :around advice
                '((name . override-todo-timestamp-once)))))

有同样痛点的朋友可以试试。

7 个赞

又学到一招,unwind-protect, 我之前遇到过想临时加advice的问题,不知道有这个东西。

为啥不用 ++1w

1 个赞