周末时,我拜读了 denote 的文档,并尝试给自己的工作流从原先的 org-roam + logseq 的模式,换成了 denote ,并结合自己做的脚本,基本覆盖了日常工作场景,并有了许多对 emacs 新的理解。

denote 是 Protesilaos Stavrou 推出的一个笔记工具,地址在 denote

特点

这个工具的最大特点是不预设用户会用什么样的工作流进行日常工作,所以在介绍时放了大量的篇幅用来介绍如何用各种 api 创建文档。也不像现在的许多现有的工具,都实现假定用户一定是用 org-mode 作为格式。

denote 支持 org-mode, txt, markdown 三种格式,并用一套文件名生成规则来唯一确定一个生成的文件。不像 org roam , denote 把时间、标题、tag 都直接放在了文件名上,并且提供一系列的 api 专门用来重命名这些文件。 这是一个非常正确且有趣的思路,本身文件就是用文件名唯一区别的,对文件的预览、搜索、修改、标记等的现成的工具也非常多,不需要再重新发明轮子。 如 org roam 这样的工具,把文件名只是用唯一的 id 标记,或如 logseq 这样的,文件名上全无唯一标记的内容,我们不得不用专用的工具来解决文件查找,或去重的操作。以文件名这一文件的本身属性来做为内容标记,是个漂亮设计,充分体现了 hacker 精神。

另外 denote 不提供专门的 mode ,他真的只是一系列创建文件,修改文件,连接文件的 api ,甚至连搜索都不提供,尽可能的让用户复用原有的 emacs 功能。 作为对比, org-roam-mode 下,我许多 org-mode 的配置会失效,同时如上所说,我还不得不再次配置我的工具如 deft 来识别 org-roam 的 title。

总而言之, denote 是一款简单,可定制能力强,使用自由,理念先进的笔记工具,他大致实现的功能有

  • 一系列创建 note 的方法,可以和自己的其他过程结合,也可以和 org-capture 结合
  • 一系列生成和修改 note 文件名的方法,作为唯一标记
  • 连接一个给定文件的方法,可以和任何的搜索工具结合
  • 搜索和展示 backlinks 的方法

我的使用场景

我的日常使用场景分3个大块:

  1. GTD: 使用的是 org-agenda 配合独立的 todo.org 文件。我每天都有数十个项目和会议记录在这个文件里,数量比较多,且属性很多层级复杂,所以我不会直接去编辑这些 org 文件,而是把 GTD 操作函数化了,直接在 agenda 里操作
  2. Journal: 作为工作笔记,零散的思考。每天一篇,在工作的时候想到什么写什么,用来记录上下文。这样在工作中断后,还能继续之前的思路。这样的思考过程是提纲式的,所以 org-mode 很适合这样的笔记形式。
  3. Note: 作为一个长期记录的话题,他可以是一个课题,一个正在执行的项目,一段对别的资料的思考延伸。笔记和笔记之间会有用 link 联系起来。一些重点的 Note 会用 bookmark 放在显眼的地方,随手取用,随手记录。

就做笔记的目的而言,我不是为了形成百科全书式的知识库,事实上现在最好的知识库都不如 google 或者 Wikipedia , 我个人完全没有必要去花费精力去构建这样一个知识体系。 我做笔记的目的,是随时记录思考,并把思考相互串联,帮助我在需要时,快速回忆起当时的点,为下一个思考做依据。 最终在大量的思考笔记后,这些内容会形成 blog ,会形成技术文档,会形成 PPT 做汇报,会形成软件发布,这些都不是在笔记中完成的。

所以我的笔记不需要复杂的功能,不需要可视化的富文本结构,只要有快速记录,内容之间的连接,并符合我日常的需求即可。

一个日常的工作流程如下:

  • 每天打开当天的 Journal 文件,记录任何的新想法
  • 用 org-capture 来捕捉任务,用专门的 org 文件来记录所有的 GTD 项目,用 org-agenda 管理
  • 如果有明确类别的记录,比如一个项目笔记,或读书笔记,使用 denote-capture 来快速捕捉成一篇笔记
  • 在次日的早晨,回顾前一天的 Journal, 并把有价值的内容 refile 成一偏单独的笔记,以供以后检索
  • 在创建笔记或回顾某个笔记时,可以通过 denote-link 来连接两个相关的笔记. denote 也会自动为其创建 backlinks

所有的这些工作基本都是在桌面端完成。我曾经尝试过许多移动端的笔记方案,但实践后发现,移动端并不适合记录长篇的内容,随手记点备忘可能更是需求点,所以我使用 borg + icloud 的方式同步 GTD 项目。

另一方面我也会有移动端上想要找某个笔记的需求,所以我把我的目录配置完全和 logseq 的兼容,通过 icloud 进行数据同步。

我的配置方式

引入 denote

(use-package denote
  :config
  ;;; 我使用 icloud 做多设备同步
  (setq denote-directory (expand-file-name "roam/" org-base-path-icloud)        
        denote-known-keywords '("emacs" "work" "blog" "journal")
        denote-infer-keywords t
        denote-sort-keywords t
        denote-date-prompt-use-org-read-date t
        ;;; 配置目录结构,让其与 logseq 的兼容,这样就能通过 icloud 在移动端读取笔记
        denote-journal-home (expand-file-name "journals/" denote-directory)
        denote-note-home (expand-file-name "pages/" denote-directory)
        )
  )

创建 Journal

  ;;; 根据日期创建或打开一篇 journal
  (defun my-denote-journal-with-date (date)
	"Create an entry tagged 'journal' and the other 'keywords' with the date as its title, there will be only one entry per day."
    ;;; 如果没传日期,则使用日历选择一个日期创建
	(interactive (list (denote-date-prompt)))
	(let* ((formatted-date (format-time-string "%Y-%m-%d" (denote--valid-date date)))
		   (entry-of-date-regex (concat "^[^\\.].*" formatted-date))
		   (entry-of-date (car (directory-files denote-journal-home nil entry-of-date-regex)))
		   )

	  (if entry-of-date
		  (find-file (expand-file-name entry-of-date denote-journal-home))
		(denote
		 formatted-date
		 '("journal")
		 nil
		 denote-journal-home)
		)))

  ;;; 创建或打开今天的 journal 
  (defun my-denote-journal-for-today ()
    "Write a journal entry for today."
    (interactive)
    (my-denote-journal-with-date
     (format-time-string "%Y-%m-%dT00:00:00")))

有时候也可能会有这样的情况,我在 GTD 项目里记录了一个会议,需要扩展出一篇会议记要。我个人习惯在 GTD 项目中用 clock-in 功能记录会议时长,同时做会议笔记。当完成这个会议后,我会把整个项目 refile 到 journal 上。

于是我再实现一个 split subtree 的方法

  (defun my-denote-split-org-subtree-to-journal()
    "Refile the org subtree as a node of the journal"
    (interactive)
    
    (org-copy-subtree)
    (delete-region (org-entry-beginning-position) (org-end-of-subtree))
    (my-denote-journal-for-today)
    (end-of-buffer)
    (org-return)
    (org-yank))

创建 note

  (defun my-denote-note ()
     "Create a note to pages, need to provide a title and tag"
    (interactive)
    (let ((denote-prompts '(title keywords))
          (denote-directory denote-note-home))
      (call-interactively #'denote-open-or-create)))

这里 (denote-prompts '(title keywords)) 方法是告诉 denote ,这个创建 note 的过程只要用户输入 title 和 keywords 就好了,其他的读取默认信息。然后我就在本地变量中临时的把 denote-directory 变成 note-home ,自动的会把 note 保存到不同于 journal 的位置,同时也不会影响全局变量。 这也是我从 denote 的配置过程中学习到的新技能。

另一种情况是通过 org-capture 来创建 note,需要注册一个 org-capture-template 来实现

  (defun my-capture-denote-note()
    "Capture a note to pages"
    (interactive)
    (let ((denote-directory denote-note-home))
      (denote-org-capture)))
      
  (with-eval-after-load 'org-capture
    (setq denote-org-capture-specifiers "%l\n%i\n%?")
    (add-to-list 'org-capture-templates
                 '("n" "New denote" plain
                   (file denote-last-path)
                   #'my-capture-denote-note
                   :no-save t
                   :immediate-finish nil
                   :kill-buffer t
                   :jump-to-captured t)))

同时,如前文所说,我经常会把一个 journal 上的项目 refile 成一个新 note 方便继续追踪

  (defun my-denote-split-org-subtree-to-note ()
    "Create new Denote note as an Org file using current Org subtree."
    (interactive)
    (let* ((keywords (denote--keywords-prompt))
          (text (org-get-entry))
          (heading (org-get-heading :no-tags :no-todo :no-priority :no-comment))
          (tags (org-get-tags)))

      (delete-region (org-entry-beginning-position) (org-end-of-subtree))

      (if (> (length tags) 0)
          (dolist (tag tags)
            (push tag keywords)))

      (let (path)
        (save-window-excursion
          (denote heading keywords nil denote-note-home)
          (insert text)
          (save-buffer)
          (setq path (buffer-file-name)))
        (denote-link path))
      ))

这里增加了一个步骤,在 refile 的时候,会提示再添加所需的 tag , 方便我们再次标记这个节点的类别。 同时在完成这个 refile 后,会在原位置留下新 note 的 link ,方便后续回顾上下文。

创建连接

我会需要在记某个笔记时,去插入一个已有笔记的链接,如果这个笔记不存在,我们先创建一个笔记,并再连接。

  (defun my-denote-link-or-create-note()
    "Link or create a note"
    (interactive)
    (let ((denote-directory denote-note-home))
      (call-interactively #'denote-link-or-create)))

搜索内容

denote 没有提供搜索的功能,我直接复用 counsel-rg 的方法来实现内容搜索,还能方便的应用 counsel 的扩展能力。

  (defun my-search-denote-rg(&optional INITIAL_INPUT)
    (interactive)
    (counsel-rg INITIAL_INPUT denote-directory " -g*.{org,md}"))

这里我过滤只搜索 org 和 markdown 文件

效果

Orgmode with denote

后续思考

目前的形式已经集过程了 org agenda 但由于考虑性能问题,笔记中的 todo 项目无法被集成进来。同时机于 emacs 的笔记方案都无法像现代笔记工具一样用多项目连接,或网络图的方式显示多个笔记之间的联系(可以通过外挂 web 的方式实现,但不方便),在大量笔记阅读的时候会是一个麻烦的体验。所以后续可以结合 org-ql 这个库,创建多个笔记,话题,关键词之间的联系,放在同一个视图上下文中,可能会是一个不错的方案。可能的场景有:

  • 把本周所有的 journal 全列在一个视图里用于回顾
  • 把本周 journal 里的 todo 项目全列出来,但对更早的 todo 项目直接舍弃
  • 把相互之间有 link 的笔记用一个视图全列出来,用于整理一个新笔记

等等。