emacs-自动切换带统计的标题TODO状态
在org-mode中,checkbox是很常见的一种列表:
- [-] 待办项
- [ ] 子待办项1
- [X] 子待办项2
并且checkbox有一个特点,就是在子任务完成后才会将上级项自动标记完成;子任务取消完成后上级项也会自动取消完成。
- [X] 待办项
- [X] 子待办项1
- [X] 子待办项2
那么,既然org-mode可以以大纲形式排列TODO任务,我们能不能让TODO任务也这样自动变换呢?
我们可以通过设置 org-enforce-todo-dependencies 变量为 t ,来「在任务还有子任务未完成时,阻止任务从未完成状态到完成状态的改变」(来源)。但是这一个选项是全局的,会影响到其他所有任务。
或者我们也可以通过 org-after-todo-statistics-hook ,为带有统计( [/] )的TODO项提供自动切换的函数。
1. 具体设置
首先我们需要设置两组TODO关键字。由于这里的heading会自动切换状态,因此我们用新的一组关键字 STEP 和 FINI ,以当在有需要的时候,这些自动管理的事项不会出现在我们可能的agenda/org-ql等过滤结果中。
(setq org-todo-keywords '((sequence "TODO(t)" "HOLD(h)" "|" "DONE(d)" "XXXX(x)")
(sequence "STEP(s)" "|" "FINI(f)")))
;; 设置org-TODO-keywords颜色
(setq org-todo-keyword-faces `(("TODO" . (:foreground ,(modus-themes-get-color-value 'red-warmer) :weight bold))
("STEP" . (:foreground ,(modus-themes-get-color-value 'red-cooler) :weight bold))
("HOLD" . (:foreground ,(modus-themes-get-color-value 'yellow-warmer) :weight bold ))
("XXXX" . (:foreground ,(modus-themes-get-color-value 'magenta-warmer) :weight bold ))
("DONE" . (:foreground ,(modus-themes-get-color-value 'green-warmer) :weight bold ))
("FINI" . (:foreground ,(modus-themes-get-color-value 'green-cooler) :weight bold ))))
如果你没有安装 modus-themes 或者 ef-themes ,可以自行指定颜色,这里以gruvbox配色为例:
;; 设置org-TODO-keywords颜色
(setq org-todo-keyword-faces '(("TODO" . (:foreground "#cc241d" :weight bold))
("STEP" . (:foreground "#9d0006" :weight bold))
("HOLD" . (:foreground "#d79921" :weight bold ))
("XXXX" . (:foreground "#b16286" :weight bold ))
("DONE" . (:foreground "#98971a" :weight bold ))
("FINI" . (:foreground "#79740e" :weight bold ))))
然后我们为 org-after-todo-statistics-hook 变量添加一个函数:
(defun my/org-heading-as-checkbox (n-done n-not-done)
"根据子任务进度 Cookie 的变化,自动更新父级标题的 TODO 状态。"
(let* ((org-log-done nil) ;; 自动变更时不触发 CLOSED 时间戳或弹出备注窗口,保持清爽
(org-todo-log-states nil)
(state (org-get-todo-state))) ;; 获取触发统计的那个父标题的当前状态
(if (and state (= n-not-done 0))
;; 1. 如果子任务全部标记完成 (未完成数量为 0) [[ [[https://www.nongnu.org/org-edna-el/][Org Edna]]]]
(when (string= state "STEP") (org-todo "FINI")) ;; 子步骤自动变成 FINI
;; 2. 如果还有子任务未完成 (没有全部标记完成),它会自动退回。
(when (string= state "FINI") (org-todo "STEP")))))
;; 将该函数挂载到 TODO 统计更新的触发器上
(add-hook 'org-after-todo-statistics-hook 'my/org-heading-as-checkbox)
这样一来,在有 STEP 或 FINI 关键字且结尾有进度统计cookie [/] 或 [%] 的事项及其子项中,会根据进度情况自动切换到完成 FINI 和未完成 STEP 。
如果因为手动编辑的原因导致没有自动触发,可以按 C-c # 全局刷新统计cookie。
注意:该函数硬编码了 STEP 和 FINI 关键字,如果你自定义了其他名字,记得同步修改函数中的字符串。
* STEP 项目 A [1/2] <-- 必须有这个统计的cookie [1/2] 才会触发自动切换
** DONE 子任务 1
** TODO 子任务 2
1.1. 补充说明
1.1.1. 为何要设置一个单独的TODO关键词组
考虑如下情况:
* TODO 总任务[1/3]
** TODO 子任务1
** TODO 子任务2[0/3]
*** TODO 子子任务1
*** TODO 子子任务2
** DONE 子任务3
在这种情况下,子任务需要设置TODO关键字,否则会被总任务的统计cookie忽略;子任务没有手动管理的必要,在子子任务均完成后就应当被标记为完成。如果沿用旧的TODO关键词,这种自动管理的任务就会被 org-ql 或者 org-agenda 、 org-sparse-tree 等函数捕获到,污染我们的结果页面。
因此最佳的选择是新建一组TODO关键字,仅用于自动化任务的状态设置:
* TODO 总任务[0/3]
** STEP 子任务1
** STEP 子任务2[0/3]
*** TODO 子子任务1
*** TODO 子子任务2
** FINI 子任务3
1.1.2. 更加高级、详尽的自动化
我使用过 Org Edna ,不过目前个人任务管理中还用不上这么细致的插件,因此暂时移除了。如果有需要,可以查看相关文章: