用 org-journal 搭建轻量 GTD 工作流
本文为个人配置的介绍与分享,而非 org-journal 本身的使用教程,更多信息参见 org-journal github 示例,完整配置见文章结尾
开始
从零开始搭建 org-mode 知识库或从其他软件迁移到 org-mode 无疑是十分累人的,而在系统打磨工作流程时我们也仍需要基础的、便于迁移的笔记工具,org-journal 就是这样一个功能强大且易于上手的 package,配合一些简单的自定义函数可以十分快速地搭建起一套低学习成本的,基于日记的日程管理机制。
Functions to maintain a simple personal diary / journal using in Emacs.
安装
首先,使用 use-package 或其他你习惯的方式安装 org-journal
(use-package org-journal)
;; 可以将以下的配置都放入 use-package 的 :config 中
然后为 org-journal 指定一个文件夹。
(setq org-journal-dir "你的日记文件夹")
模板
org-journal 支持以天、周、月为组创建日记文件,为了更好地与 Emacs 与 org-mode 的其他相关功能联动,本文以最常见的一天一文件为例。
org-journal 本身提供了类似如下片段的日记模板:
* Tuesday, 06/04/13
** 10:28 Company meeting
Endless discussions about projects. Not much progress
** 11:33 Work on org-journal
For the longest time, I wanted to have a cool diary app on my
computer. However, I simply lacked the right tool for that job. After
many hours of searching, I finally found PersonalDiary on EmacsWiki.
PersonalDiary is a very simple diary system based on the emacs
calendar. It works pretty well, but I don't really like that it only
uses unstructured text.
Thus, I spent the last two hours making that diary use org-mode
and represent every entry as an org-mode headline. Very cool!
** 15:33 Work on org-journal
Now my journal automatically creates the right headlines (adds the
current time stamp if on the current day, does not add a time stamp
for any other day). Additionally, it automatically collapses the
headlines in the org-file to the right level (shows everything if in
view mode, shows only headlines in new-entry-mode). Emacs and elisp
are really cool!
** 16:40 Work on org-journal
I uploaded my journal mode to marmalade and Github! Awesome!
** TODO teach org-journal how to brew coffee
所谓配置实际就是修改与优化它的结构,此处的核心是这几个变量
org-journal-file-format- 日记文件的文件名格式
org-journal-file-header- 日记开头自动插入的文件头
org-journal-date-format- 文件头之后创建的日记模板
org-journal-time-prefix- 在文件中创建的项(上图中的二级标题)的前缀格式
org-journal-time-format- 创建的项的标题内容与属性格式
org-journal-carryover-items- 需要继承到下一天的 TODO 项
这是配置与简单讲解:
(setq org-journal-file-format "%Y-%m-%d.org" ;; 在日记目录中创建如 2026-1-1.org 格式的文件
org-journal-file-header "#+title: Daily Journal\n#+category: journal\n#+STARTUP: content\n\n" ;; 插入简单的文件头,设置 category 便于使用 agenda 等工具浏览与检索
org-journal-date-format (lambda (time) ;; 为日记项,也就是一级标题添加属性,"[]" 表示不活跃时间戳,不会在 agenda 中显示该项
(format "* %s %s\n:PROPERTIES:\n:Created: [%s]\n:END:"
(format-time-string "%Y-%m-%d" time)
(format-time-string "%a" time)
(format-time-string "%Y-%m-%d %a" time)))
org-journal-time-prefix "\n*** TODO " ;; 新建项的前缀,此处设为三级标题的 TODO 项
org-journal-time-format "%m-%d \n:PROPERTIES:\n:Created: <%Y-%m-%d %a %H:%M:%S>\n:END:\n" ;; 新建项的内容,插入当前时间 "%m-%d"并添加属性,"<>" 表示活跃时间戳
org-journal-hide-entries-p t ;; 创建新项时折叠其他标题
org-journal-carryover-items "TODO=\"TODO\"|TODO=\"STARTED\"|TODO=\"WAITING\"|TODO=\"SOMEDAY\"") ;; 将继承到下一天的 TODO 项,如果有额外的自定义 TODO 可以手动添加
如果你不希望让日记中只剩下这些严肃的 TODO ,可以引入一个函数:
(defun my/journal-setup-new-day ()
(save-excursion
(org-back-to-heading)
(let ((day-end (save-excursion (org-end-of-subtree) (point))))
(unless (search-forward "** daily" day-end t)
(goto-char day-end)
(insert "\n\n** daily\n\n\n")
(insert "** tasks\n")))))
(add-hook 'org-journal-mode-hook 'font-lock-update) ;; 记得添加 hook
这样会将 daily 与 tasks 作为两个标题区分开。
现在执行 org-journal-new-entry 后,创建的日记文件看起来大概是:
#+title: Daily Journal
#+category: journal
#+STARTUP: content
* 2026-04-03 Fri
:PROPERTIES:
:Created: [2026-04-03 Fri]
:END:
** daily
这里是平常的日记
** tasks
*** TODO 04-03 这里是任务
:PROPERTIES:
:Created: <2026-04-03 Fir 04:59:15>
:END:
继承 TODO
org-journal 最重要的功能之一就是自动转移之前文件中的 TODO 项,但在默认情况下这会导致之前文件中的 TODO 被删除,而如果不删除旧文件中的 TODO 又会导致 agenda 被不同文件中反复继承的同一 TODO 项填满,此处可以利用 org-mode 的 COMMENT 关键字,自动将旧文件中的 TODO 状态改为 COMMENT。
(defun my/org-journal-mark-old-todos-as-comment (entries)
"Change TODO entry's TODO keyword to COMMENT after carryover."
(save-excursion
(let* ((todo-keywords (cl-remove-if
(lambda (kw) (member kw org-done-keywords))
org-todo-keywords-1))
(todo-pattern (when todo-keywords
(regexp-opt todo-keywords 'words))))
(when todo-pattern
(mapc (lambda (entry)
(let ((start (car entry))
(end (set-marker (make-marker) (cadr entry))))
(goto-char start)
(while (re-search-forward (concat "^\\(\\*+ \\)" todo-pattern) end t)
(replace-match "\\1COMMENT" t nil))
(set-marker end nil)))
(reverse entries))))
(save-buffer)))
(setq org-journal-handle-old-carryover-fn #'my/org-journal-mark-old-todos-as-comment)
为防止出现部分情况下不跳过 COMMENT 的情况,可以手动指定需要跳过的关键字:
(setq org-agenda-skip-function-global
'(org-agenda-skip-entry-if 'todo '("COMMENT")))
优化
自动跳转光标
新建 TODO 项后移动光标到标题尾部再开始打字显得实在麻烦,使用
(defun my/journal-position-cursor-after-time ()
(when (derived-mode-p 'org-journal-mode)
(org-back-to-heading)
(end-of-line)))
(add-hook 'org-journal-after-entry-create-hook #'my/journal-position-cursor-after-time)
即可在创建新项后将光标快速放至标题行尾。
在 Calendar 中快速打开日记
在回看曾经的日记时,使用 Emacs 的 Calendar 功能会十分便利,但 org-journal 并没有提供直接通过 Calendar 打开相应日记的功能,这里可以使用此函数:
(defun org-journal-open-entry-for-editing (&optional event)
"Open journal entry for the date under cursor in calendar for editing."
(interactive
(list last-nonmenu-event))
(let* ((date (calendar-cursor-to-date t event))
(time (org-journal--calendar-date->time date))
(org-journal-file (org-journal--get-entry-path time)))
(if (file-exists-p org-journal-file)
(progn
(funcall org-journal-find-file org-journal-file)
(unless (org-journal--daily-p)
(org-journal--goto-entry date)))
(message "No journal entry for this date."))))
刷新高亮
为了一些额外功能,在日记文件中 org-journal 会自动开启一个叫做 org-journal-mode 的 major mode,但此模式在某些情况下可能会出现渲染 bug,需要刷新语法高亮:
(add-hook 'org-journal-mode-hook 'font-lock-update)
清理 buffer list
加载 org-agenda 会直接打开所有相关文件,给 buffer 的切换与管理造成许多不便,但有许多方法可以改善,如果你在使用 perspective 与 consult,只需要添加如下配置:
(with-eval-after-load 'consult ;; 在 consult-buffer 中隐藏当前 perspective 之外的 buffer,包括 agenda 可能打开的 org 文件
(consult-customize consult-source-buffer :hidden t :default nil)
(add-to-list 'consult-buffer-sources persp-consult-source))
(setq switch-to-prev-buffer-skip ;; 使用 previous-buffer 等命令切换 buffer 时跳过当前 perspective 之外的 buffer
(lambda (win buff bury-or-kill)
(not (persp-is-current-buffer buff))))
如果有时仍需要管理 perspective 之外的 buffer 可以使用 consult 的 narrow 功能和 ibuffer,或直接使用 persp-switch-to-buffer。更多 perspective 相关内容阅读 org-ql github仓库示例。
完整配置
(use-package org-journal
:config
(setq org-journal-dir "你的日记目录")
(setq org-journal-file-format "%Y-%m-%d.org"
org-journal-file-header "#+title: Daily Journal\n#+category: journal\n#+STARTUP: content\n\n"
org-journal-date-format (lambda (time)
(format "* %s %s\n:PROPERTIES:\n:Created: [%s]\n:END:"
(format-time-string "%Y-%m-%d" time)
(format-time-string "%a" time)
(format-time-string "%Y-%m-%d %a" time)))
org-journal-time-prefix "\n*** TODO "
org-journal-time-format "%m-%d \n:PROPERTIES:\n:Created: <%Y-%m-%d %a %H:%M:%S>\n:END:\n"
org-journal-hide-entries-p t
org-journal-carryover-items "TODO=\"TODO\"|TODO=\"STARTED\"|TODO=\"WAITING\"|TODO=\"SOMEDAY\"")
(defun my/journal-setup-new-day ()
(save-excursion
(org-back-to-heading)
(let ((day-end (save-excursion (org-end-of-subtree) (point))))
(unless (search-forward "** daily" day-end t)
(goto-char day-end)
(insert "\n\n** daily\n\n\n")
(insert "** tasks\n")))))
(add-hook 'org-journal-after-header-create-hook #'my/journal-setup-new-day)
(defun my/org-journal-mark-old-todos-as-comment (entries)
"Change TODO entry's TODO keyword to COMMENT after carryover."
(save-excursion
(let* ((todo-keywords (cl-remove-if
(lambda (kw) (member kw org-done-keywords))
org-todo-keywords-1))
(todo-pattern (when todo-keywords
(regexp-opt todo-keywords 'words))))
(when todo-pattern
(mapc (lambda (entry)
(let ((start (car entry))
(end (set-marker (make-marker) (cadr entry))))
(goto-char start)
(while (re-search-forward (concat "^\\(\\*+ \\)" todo-pattern) end t)
(replace-match "\\1COMMENT" t nil))
(set-marker end nil)))
(reverse entries))))
(save-buffer)))
(setq org-journal-handle-old-carryover-fn #'my/org-journal-mark-old-todos-as-comment)
(setq org-agenda-skip-function-global
'(org-agenda-skip-entry-if 'todo '("COMMENT")))
(defun my/journal-position-cursor-after-time ()
(when (derived-mode-p 'org-journal-mode)
(org-back-to-heading)
(end-of-line)))
(add-hook 'org-journal-after-entry-create-hook #'my/journal-position-cursor-after-time)
(defun org-journal-open-entry-for-editing (&optional event)
"Open journal entry for the date under cursor in calendar for editing."
(interactive
(list last-nonmenu-event))
(let* ((date (calendar-cursor-to-date t event))
(time (org-journal--calendar-date->time date))
(org-journal-file (org-journal--get-entry-path time)))
(if (file-exists-p org-journal-file)
(progn
(funcall org-journal-find-file org-journal-file)
(unless (org-journal--daily-p)
(org-journal--goto-entry date)))
(message "No journal entry for this date."))))
(add-hook 'org-journal-mode-hook 'font-lock-update))