UP | HOME

▼ 本文更新于 [2026-03-13 周五 17:13]

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会自动切换状态,因此我们用新的一组关键字 STEPFINI ,以当在有需要的时候,这些自动管理的事项不会出现在我们可能的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)

这样一来,在有 STEPFINI 关键字且结尾有进度统计cookie [/][%] 的事项及其子项中,会根据进度情况自动切换到完成 FINI 和未完成 STEP

如果因为手动编辑的原因导致没有自动触发,可以按 C-c # 全局刷新统计cookie。

注意:该函数硬编码了 STEPFINI 关键字,如果你自定义了其他名字,记得同步修改函数中的字符串。

* 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-agendaorg-sparse-tree 等函数捕获到,污染我们的结果页面。

因此最佳的选择是新建一组TODO关键字,仅用于自动化任务的状态设置:

* TODO 总任务[0/3]
** STEP 子任务1
** STEP 子任务2[0/3]
*** TODO 子子任务1
*** TODO 子子任务2
** FINI 子任务3

1.1.2. 更加高级、详尽的自动化

我使用过 Org Edna ,不过目前个人任务管理中还用不上这么细致的插件,因此暂时移除了。如果有需要,可以查看相关文章:

© Published by Emacs 31.0.50 (Org mode 10.0-pre) | RSS Comment