大家在用 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 掉,让它认为“现在”就是我们指定的时间。
用法很简单:
M-x my/set-next-org-todo-time,选一个时间(支持 org-read-date 的各种骚操作,比如-3)。- 接着按
C-c C-t切换状态。 - 完事。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)))))
有同样痛点的朋友可以试试。