¶简介
Org Roam 是一种非常好的工具,可以编写和组织您的想法,但是当您利用 Org Roam 提供的更多功能时,您可以为常见任务创建高效的自定义工作流程。
在本视频中,我将向您展示 5 个在 Org Roam 和 Org Mode 特性的帮助下优化笔记记录和项目管理工作流程的技巧。
¶开始配置
开始之前,请确保您已经设置好 Org Roam。
您可以通过复制以下配置快速入手,但是如果首先观看 Org Roam 系列的上一集视频,您会学到更多内容:
(use-package org-roam :ensure t :init (setq org-roam-v2-ack t) :custom (org-roam-directory "~/RoamNotes") (org-roam-completion-everywhere t) :bind (("C-c n l" . org-roam-buffer-toggle) ("C-c n f" . org-roam-node-find) ("C-c n i" . org-roam-node-insert) :map org-mode-map ("C-M-i" . completion-at-point) :map org-roam-dailies-map ("Y" . org-roam-dailies-capture-yesterday) ("T" . org-roam-dailies-capture-tomorrow)) :bind-keymap ("C-c n d" . org-roam-dailies-map) :config (require 'org-roam-dailies) ;; Ensure the keymap is available (org-roam-db-autosync-mode))
¶有很多代码要展示!
在本视频中,我将向您展示许多自定义代码,这些代码产生了这些工作流程改进。如果您目前还没有深入研究 Emacs Lisp,其中有些内容可能不完全清晰,但这没关系!
所有代码均可取并放入您的配置中,只要它正常工作,您无需理解其工作原理!
如果您确实想学习,我建议阅读代码并使用 M-x describe-function
和 M-x describe-variable
阅读文档字符串。如果您想要一个更完整的教程,也可以观看我的 学习 Emacs Lisp 系列!
请在评论中提出任何疑问,我将尽力澄清!
¶更快速地插入笔记以实现更流畅的写作
有时在写作时,您可能希望在 Org Roam 笔记中创建一个新节点而不中断写作流程!通常您将使用 org-roam-node-insert
,但当您使用此命令创建新笔记时,该新笔记将在创建后打开。
我们可以定义一个函数,使您可以创建新笔记并在当前文档中插入链接,而不打开新笔记的缓冲区。
这将使您能够在写作时快速为您提到的主题创建新笔记,以便以后可以在这些笔记中填写更多细节!
;; 将此绑定为 C-c n I (defun org-roam-node-insert-immediate (arg &rest args) (interactive "P") (let ((args (cons arg args)) (org-roam-capture-templates (list (append (car org-roam-capture-templates) '(:immediate-finish t))))) (apply #'org-roam-node-insert args)))
此函数采取 org-roam-capture-templates
中的第一个捕获模板(通常是“默认”模板),并向其添加 :immediate-finish t
捕获属性,以在捕获完成后防止加载笔记缓冲区:
感谢 Umar Ahmad 提供的代码片段!
¶从 Org Roam 笔记构建 Org 议程
Org Mode 最有用的功能之一是议程视图。实际上,您可以使用 Org Roam 笔记作为此视图的源!
通常,您不会希望拉入 所有 Org Roam 笔记,因此我们只使用具有特定标签(如 Project
)的笔记。
下面是一个代码片段,它将找到具有特定标签的所有笔记,然后使用相应的笔记文件设置您的 org-agenda-list
。
;; 包含此代码的缓冲区必须将 lexical-binding 设置为 t! ;; 有关更多详细信息,请参阅最终配置。 (defun my/org-roam-filter-by-tag (tag-name) (lambda (node) (member tag-name (org-roam-node-tags node)))) (defun my/org-roam-list-notes-by-tag (tag-name) (mapcar #'org-roam-node-file (seq-filter (my/org-roam-filter-by-tag tag-name) (org-roam-node-list)))) (defun my/org-roam-refresh-agenda-list () (interactive) (setq org-agenda-files (my/org-roam-list-notes-by-tag "Project"))) ;; 首次为会话生成议程列表 (my/org-roam-refresh-agenda-list)
现在运行 M-x org-agenda
查看 org 议程,然后按 a
查看每日时间表或按 d
查看项目文件中的所有 TODO
列表。
为了获得最佳结果,请确保将所需的标签(在本例中为 Project
)添加到作为捕获模板的一部分的新笔记文件。只记住在使用该标签创建新笔记后调用 my/org-roam-refresh-agenda-list
来刷新列表!
注意:我还没有找到可靠而有效的方法将日常事项拉入议程!一旦找到,我可能会制作另一个视频。
¶提示:改善议程视图中笔记的外观
您可能会注意到,来自您的 Org Roam 文件的议程行由于带时间戳的文件名而看起来有点难看。我们可以通过在项目文件之一的标题行添加 category
来修复此问题,如下所示:
#+title: Mesche #+category: Mesche #+filetags: Project
通常,您会希望类别包含与笔记相同的名称,以便我们可以更新来自 Org Roam 第2集 的 Project 模板以自动包含它:
("p" "project" plain "* Goals\n\n%?\n\n* Tasks\n\n** TODO Add initial tasks\n\n* Dates\n\n" :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Project") :unnarrowed t)
这会将标题和类别设置为新笔记文件的名称,从而在议程视图中产生更清晰的条目。
¶从具有特定标签的笔记列表中选择
org-roam-node-find
函数给了我们过滤显示用于选择的笔记列表的能力。
我们可以定义自己的函数,为具有特定标签(如我们之前讨论过的 Project
)的笔记显示选择列表。这对于设置快速选择特定笔记集的键绑定很有用!
一个额外的好处是我们可以覆盖创建新笔记时使用的捕获模板集。
这意味着如果笔记尚不存在,我们可以自动使用项目捕获模板创建新笔记!
(defun my/org-roam-project-finalize-hook () "如果捕获未被中止,则将捕获的项目文件添加到`org-agenda-files'中。" ;; 由于临时添加了钩子,所以删除钩子 (remove-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook) ;; 如果确认捕获,则将项目文件添加到议程列表中 (unless org-note-abort (with-current-buffer (org-capture-get :buffer) (add-to-list 'org-agenda-files (buffer-file-name))))) (defun my/org-roam-find-project () (interactive) ;; 捕获完成后,将项目文件添加到议程中 (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook) ;; 选择要打开的项目文件,如有必要则创建项目文件 (org-roam-node-find nil nil (my/org-roam-filter-by-tag "Project") :templates '(("p" "project" plain "* Goals\n\n%?\n\n* Tasks\n\n** TODO Add initial tasks\n\n* Dates\n\n" :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Project") :unnarrowed t)))) (global-set-key (kbd "C-c n p") #'my/org-roam-find-project)
此代码片段的一个有用方面是 org-capture-after-finalize-hook
允许我们确保新的项目笔记通过调用我们之前定义的 my/org-roam-project-finalize-hook
函数自动添加到 Org 议程中!
¶简化自定义捕获任务和笔记
Org Roam 提供了一个名为 org-roam-capture-
的低级函数(是的,有连字符!),允许您以非常灵活的方式调用笔记捕获功能。更多信息可以在 Org Roam 手册中找到:扩展捕获系统。
我们可以使用此函数来优化捕获工作流程的特定部分!
这里有一些您可能使用它的方式:
¶保留笔记和任务的收件箱
如果您想使用单个键绑定快速捕获新笔记和任务,以便稍后审查,我们可以使用 org-roam-capture-
将其捕获到单个特定文件,如 Inbox.org
!
即使此文件不会有时间戳文件名,它也仍然会被视为 Org Roam 笔记中的节点。
(defun my/org-roam-capture-inbox () (interactive) (org-roam-capture- :node (org-roam-node-create) :templates '(("i" "inbox" plain "* %?" :if-new (file+head "Inbox.org" "#+title: Inbox\n"))))) (global-set-key (kbd "C-c n b") #'my/org-roam-capture-inbox)
¶直接捕获任务到特定项目
如果您设置了之前提到的项目笔记文件,您可以设置一个捕获模板,让您快速为任何项目捕获任务。
与前面的示例一样,我们可以选择已存在的项目,或者在项目笔记不存在时自动创建项目笔记!
(defun my/org-roam-capture-task () (interactive) ;; 捕获完成后,将项目文件添加到议程中 (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook) ;; 如有必要,捕获新任务并创建项目文件 (org-roam-capture- :node (org-roam-node-read nil (my/org-roam-filter-by-tag "Project")) :templates '(("p" "project" plain "* TODO %?" :if-new (file+head+olp "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Project" ("Tasks")))))) (global-set-key (kbd "C-c n t") #'my/org-roam-capture-task)
要注意的一点是,我们在捕获模板中使用 file+head+olp
,这样我们就可以将新任务条目放在“Tasks”标题下。
我们还使用之前定义的 my/org-roam-project-finalize-hook
函数,以便任何新项目都会被添加到 Org 议程中!
¶自动将完成的任务复制(或移动)到每日文件
每日文件一个有趣的用途是保留完成任务的记录。如果我们可以自动将任何 Org 模式文件中的完成任务复制到当天的每日文件,那会怎么样?
我们可以通过添加一些自定义代码来实现这一目的!
以下代码段为所有 Org 任务状态更改设置了一个钩子,然后将完成的(DONE
)条目复制到当天的笔记文件:
(defun my/org-roam-copy-todo-to-today () (interactive) (let ((org-refile-keep t) ;; 将此设置为 nil 以删除原件! (org-roam-dailies-capture-templates '(("t" "tasks" entry "%?" :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n" ("Tasks"))))) (org-after-refile-insert-hook #'save-buffer) today-file pos) (save-window-excursion (org-roam-dailies--capture (current-time) t) (setq today-file (buffer-file-name)) (setq pos (point))) ;; 仅在目标文件与当前文件不同时重新归档 (unless (equal (file-truename today-file) (file-truename (buffer-file-name))) (org-refile nil nil (list "Tasks" today-file nil pos))))) (add-to-list 'org-after-todo-state-change-hook (lambda () (when (equal org-state "DONE") (my/org-roam-copy-todo-to-today))))
如果要移动完成的任务而不是复制它,请在此代码中将 org-refile-keep
设置为 nil
!
此代码较高级,因此请参阅下一节以了解其工作原理!
¶工作原理
为了在 TODO
项状态更改时收到通知,我们将 my/org-roam-copy-todo-to-today
函数添加到 org-after-todo-state-change-hook
列表中。
当用户完成一个任务时,此函数将设置一个“日常”临时捕获模板,它将跳转到当天日期文件中的“Tasks”标题下。这被包装在 save-window-excursion
调用中,以确保捕获作业不会更改您的窗口配置和当前缓冲区。
如果捕获目标文件不是当前日期的文件,我们调用 org-refile
以将项目复制(或在 org-refile-keep
为 nil
时移动)到新位置!这避免了将完成的任务移动回它已经存在的文件中(这会引发错误!)。
¶最终配置
非常重要的注意事项! 确保使用此代码的配置文件顶部有以下行!
;; -*- lexical-binding: t; -*-
这可以确保在评估此配置之前加载 Org Roam,这对我们编写的自定义代码正常工作很重要。
此行启用词法绑定,可以确保 my/org-roam-filter-by-tag
函数正确工作。
(use-package org-roam :ensure t :demand t ;; Ensure org-roam is loaded by default :init (setq org-roam-v2-ack t) :custom (org-roam-directory "~/RoamNotes") (org-roam-completion-everywhere t) :bind (("C-c n l" . org-roam-buffer-toggle) ("C-c n f" . org-roam-node-find) ("C-c n i" . org-roam-node-insert) ("C-c n I" . org-roam-node-insert-immediate) ("C-c n p" . my/org-roam-find-project) ("C-c n t" . my/org-roam-capture-task) ("C-c n b" . my/org-roam-capture-inbox) :map org-mode-map ("C-M-i" . completion-at-point) :map org-roam-dailies-map ("Y" . org-roam-dailies-capture-yesterday) ("T" . org-roam-dailies-capture-tomorrow)) :bind-keymap ("C-c n d" . org-roam-dailies-map) :config (require 'org-roam-dailies) ;; Ensure the keymap is available (org-roam-db-autosync-mode)) (defun org-roam-node-insert-immediate (arg &rest args) (interactive "P") (let ((args (push arg args)) (org-roam-capture-templates (list (append (car org-roam-capture-templates) '(:immediate-finish t))))) (apply #'org-roam-node-insert args))) (defun my/org-roam-filter-by-tag (tag-name) (lambda (node) (member tag-name (org-roam-node-tags node)))) (defun my/org-roam-list-notes-by-tag (tag-name) (mapcar #'org-roam-node-file (seq-filter (my/org-roam-filter-by-tag tag-name) (org-roam-node-list)))) (defun my/org-roam-refresh-agenda-list () (interactive) (setq org-agenda-files (my/org-roam-list-notes-by-tag "Project"))) ;; Build the agenda list the first time for the session (my/org-roam-refresh-agenda-list) (defun my/org-roam-project-finalize-hook () "Adds the captured project file to `org-agenda-files' if the capture was not aborted." ;; Remove the hook since it was added temporarily (remove-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook) ;; Add project file to the agenda list if the capture was confirmed (unless org-note-abort (with-current-buffer (org-capture-get :buffer) (add-to-list 'org-agenda-files (buffer-file-name))))) (defun my/org-roam-find-project () (interactive) ;; Add the project file to the agenda after capture is finished (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook) ;; Select a project file to open, creating it if necessary (org-roam-node-find nil nil (my/org-roam-filter-by-tag "Project") :templates '(("p" "project" plain "* Goals\n\n%?\n\n* Tasks\n\n** TODO Add initial tasks\n\n* Dates\n\n" :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Project") :unnarrowed t)))) (defun my/org-roam-capture-inbox () (interactive) (org-roam-capture- :node (org-roam-node-create) :templates '(("i" "inbox" plain "* %?" :if-new (file+head "Inbox.org" "#+title: Inbox\n"))))) (defun my/org-roam-capture-task () (interactive) ;; Add the project file to the agenda after capture is finished (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook) ;; Capture the new task, creating the project file if necessary (org-roam-capture- :node (org-roam-node-read nil (my/org-roam-filter-by-tag "Project")) :templates '(("p" "project" plain "** TODO %?" :if-new (file+head+olp "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Project" ("Tasks")))))) (defun my/org-roam-copy-todo-to-today () (interactive) (let ((org-refile-keep t) ;; Set this to nil to delete the original! (org-roam-dailies-capture-templates '(("t" "tasks" entry "%?" :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n" ("Tasks"))))) (org-after-refile-insert-hook #'save-buffer) today-file pos) (save-window-excursion (org-roam-dailies--capture (current-time) t) (setq today-file (buffer-file-name)) (setq pos (point))) ;; Only refile if the target file is different than the current file (unless (equal (file-truename today-file) (file-truename (buffer-file-name))) (org-refile nil nil (list "Tasks" today-file nil pos))))) (add-to-list 'org-after-todo-state-change-hook (lambda () (when (equal org-state "DONE") (my/org-roam-copy-todo-to-today))))