ZMonster's Blog 巧者劳而智者忧,无能者无所求,饱食而遨游,泛若不系之舟

强大的 Org mode(4): 使用 capture 功能快速记录

本文是《强大的 Org mode》系列的第四篇文章,系列文章如下:

  1. 强大的 Org mode(1): 简单介绍与基本使用 · ZMonster's Blog
  2. 强大的 Org mode(2): 任务管理 · ZMonster's Blog
  3. 强大的 Org mode(3): 表格的基本操作及公式、绘图 · ZMonster's Blog
  4. 强大的 Org mode(4): 使用 capture 功能快速记录 · ZMonster's Blog

简介

Capture 是 Org mode 中非常重要的一个功能,使用它可以让我们快速地新建内容到特定的 Org mode 文件中去,具体一点,可以有下面这些场景

  • 新建一条笔记到 inbox.org 中,将剪贴板中的内容自动插入,并且附上当时的时间

    org-capture-note.gif

  • 新增一条日志,按照「年-月-日」的层级结构插入到 journal.org 中,如下图所示

    org-capture-journal.gif

  • 以表格的形式,新增一条消费支出记录到用于存放备忘信息的 memo.org 中

    org-capture-2.gif

  • 新增一条任务到 task.org 中,并且开始计时

    org-capture-task.gif

  • 新增一个代码片段到 snippet.org 中

    org-capture-snip.gif

上述看似很不一样的操作,只需要在配置里设置不同的 capture 模板即可,模板里支持的元素很多,甚至能在模板里写 elisp 代码来做到已有模板元素不能做到的事情。在写好模板并加载后,我们只需要调用 org-capture 这个函数,就能在弹出的临时 buffer 里选择对应的模板来记录不同的内容,而不用耗费精力去记忆应该打开哪个文件。

org-capture-buffer.png

此外,使用 capture 后将会打开一个临时的 buffer,在我们编辑好内容后轻按 C-c C-c,它就会消失无踪,因此对我们原先在做的事情的打断非常轻微。

总结一下就是:

  1. capture 可以预先设置记录内容的模板和存储入口
  2. capture 提供统一的输入入口
  3. capture 用完即走,不干扰当前工作流

如果你是一个 Org mode 用户,应该会用 Org mode 来做笔记记录、日志记录、任务管理这些事情,而这些事情,用 capture 来作为输入是非常自然而方便的,我也在这里建议,在 Org mode 环境里时,应当使用 capture 来作为主要的输入方式。

最小配置

capture 功能包含在 org 包里,所以只要安装了 org,那么直接就是能使用 capture 功能的。不过不做配置的话,那么

  • 没有快捷键可以触发功能
  • 默认只有一个用于创建任务的 Task 模板可选,并且存储在变量 org-default-notes-file 指定的文件里

默认的模板是在 org-capture-select-template 中定义的,其逻辑是,当执行 org-capture 命令的时候,如果检查到没有配置任何模板,就会使用一个默认的模板来保证不会出错,这个默认的模板如下所示

'("t" "Task" entry (file+headline "" "Tasks") "* TODO %?\n  %u\n  %a")

但是呢,如果我们连 org-default-notes-file 都没有设置,它会默认存储到 ~/.notes 中去,然后会由于这个文件不是 Org 文件而报错……

所以,假如我们想真正地使用起 org-capture 来,最小的配置工作,应该包含下述事情

  • 为 org-capture 命令设置一个快捷键
  • 设置 org-default-notes-file 变量的值为一个 Org 文件,比如说 ~/org/inbox.org

按照这个要求,可以得到最小的 org-capture 的配置如下

(global-set-key (kbd "C-c c") 'org-capture)
(setq org-default-notes-file "~/org/inbox.org")

这样,我们就能新建任务到 ~/org/inbox.org 这个文件中了,见下图示例

org-capture-minimum.gif

capture 模板的五个部分

上一节讲到,默认的 capture 模板是下面这个样子的

'("t" "Task" entry (file+headline "" "Tasks") "* TODO %?\n  %u\n  %a")

后面我们要自己添加新的模板,也是这个格式。这个模板包含五个部分,分别是

模板组成 对应默认模板中的内容 描述
key "t" 用来选择模板的字符
description "Task" 展示用的模板描述
type entry 新增内容的类型
target (file+headline "" "Tasks") 新增内容的存储位置
template "* TODO %?\n %u\n %a" 新增内容的模板

下面针对这五部分进行详细说明。

用于快速选择模板的 key

对应前面默认模板里的 "t",这个 key 可以是一个或两个字符,用来在执行 org-capture 的时候选择模板 —— 两个字符的情况是用来给模板分组的,第一个字符表示分组名,第二个字符用来选择这个分组下的实际模板。在我们有很多模板的时候,分组是非常有用的,一来可以让执行 org-capture 时显示的可选项更少,而来可以用来组织相近性质的模板以便管理。模板分组稍后一点会做详细说明,此处就先不展开了。

另外,经验证,这里的 key 是支持中文的 XD

描述模板的 description

对应前面默认模板里的 "Task",这个就是用来对模板进行描述的,方便我们正确地选择模板。

key 和 description 这两部分会在执行 org-capture 进入模板选择 buffer 后,会显示的内容,其他模板内容在模板选择 buffer 中都是不显示的,如下所示:

org-capture-buffer.png

这两部分,务必要注意

  • 不同模板的 key 不能是一样的
  • description 应当尽量清晰以减轻自己的记忆负担

设置新增内容类型的 type

对应前面默认模板里的 "entry",这个用来设置新增内容的类型,可选的类型如下表所示

type description
entry 带有 headline 的一个 Org mode 节点
item 一个列表项
checkitem 一个 checkbox 列表项
table-line 一个表格行
plain 普通文本

根据不同的 type,org-capture 会尝试将新增内容添加到文件中不同类型的数据的后面,比如

  • 如果 type 是 item/checkitem,那么会找到目标位置后最近的一个列表,并将新增列表项添加到这个列表的后面
  • 如果 type 是 table-line,那么会找到目标位置后最近的一个表格,并将新增行添加到表格的后面

因此对于不同的 type,要求后面的内容模板是按照一定的格式来编写的,下面是不同的 type 对应内容模板的简单示例

  • type 为 entry 时,内容模板示例

    "* headline"
    

    也就是说,template 的形式上必须是一个 headline

  • type 为 item 时

    如果内容模板为空,会插入一个普通列表项,并且等待输入;如果有需要自定义的内容,那么才需要去写内容模板。

    而此时的内容模板不需要在形式上是一个列表项,也就是说

    "- item"
    

    "item"
    

    的效果是一样的,都会在 target 对应的位置里插入下面这样一个列表项

    - item
    
  • type 为 checkitem 时

    与 type 为 item 时行为大部分一样,仅有一点区别,就是在内容模板为空的时候,它会插入一个 checkbox 列表项。

    也就是说,如果内容模板不为空,那么它其实是不保证插入的是 checkbox 列表项的,需要我们自己来保证。

    相应的内容模板应该是类似下面的格式

    "[ ] item"
    
  • type 为 table-line,内容模板示例

    "| colum 1 | colum 2 | colum3 |"
    

    就是说,内容模板必须是一个表格的行

设置新增内容写入位置的 target

对应前面默认模板里的 "(file+headline "" "Tasks")",target 用来指定

  • 新增内容要写入到哪个文件
  • 新增内容要写入到文件的什么地方

如前面的默认模板所示,target 部分用一个 list 来表示,其中第一个元素用来表示 target 的类型,可用的类型如下表所示

type description example
file 文件 (file "path/to/file")
id 特定 ID 的某个 headline (id "id of existing Org entry")
file+headline 文件的某个唯一的 headline (file+headline "path/to/file" "node headline")
file+olp 文件中的 headline 路径 (file+olp "path/to/file" "Level 1 heading" "Level 2" …)
file+regexp 文件中被正则匹配的 headline (file+regexp "path/to/file" "regexp to find location")
file+datetree 文件中当日所在的 datetree (file+datetree "path/to/file")
file+datetree+prompt 文件中的 datetree,弹出日期选择 (file+datetree+prompt "path/to/file")
file+weektree 文件中当日所在的 weektree (file+weektree "path/to/file")
file+weektree+prompt 文件中的 weektree,弹出日期选择 (file+weektree+prompt "path/to/file")
file+function 文件中被函数匹配的位置 (file+function "path/to/file" function-finding-location)
clock 当前正在计时中的任务所在的位置 (clock)
function 自定义函数匹配的位置 (function function-finding-location)

(翻译有点生硬,如有疑惑,请执行 「M-x describe-variable」并输入「org-capture-templates」查看对应的文档)

其中 file+headline 是比较常用的,用来记录笔记、创建任务一般用这个就好了。不过这个要求 headline 在文件中是唯一的,如果不是唯一的话,最好使用 file+olp,指定对应 headline 在文件中的完整路径。

而 file+datetree、file+weektree 这两种用来创建日志是非常合适的,记录的内容能按年-月-日的层级结构组织好,方便回顾和管理。

如果有自己的特殊需求,那么 file+function、function 这两个也提供了极大的自由扩展的空间。

需要注意的是,上述与文件相关的 target 类型,如果指定了文件名,那么将会优先使用这个文件名而不是变量 org-default-notes-file 指定的文件 —— 反之,如果文件部分留空,那么就会默认使用 org-default-notes-file 指定的文件了。

设置新增内容模板的 template

对应前面默认模板里的 "* TODO %?\n %u\n %a",这部分的内容是实际上新增内容的模板,通过设置它,我们可以在新增内容时

  • 自动插入时间、链接、剪贴板内容、文件内容
  • 交互式地要求输入特定内容,如 tag、headline 属性或其他自定义的字段
  • 自动插入外部应用传入的特定信息,如浏览器上当前网页的链接、选中的文本等

这部分的配置,其中的内容可以分为两类

  • 普通的文本,将会原样出现在新增内容中,如默认模板里的 "* TODO"、"\n"、" "
  • 以 % 开头的特殊标记,如 "%?" 和 "%a",将会在最后根据类型自动扩展成不同的内容

    这些特殊标记包括这些

    • 时间、日期相关

      标记 描述
      %<…> 自定义格式的 timestamp,如: %<%Y-%m-%d>,会得到 <2018-03-04 日>
      %t 当前仅包含日期的 timestamp,如: <2018-03-04 日>
      %T 当前包含日期和时间的 timestamp,如: <2018-03-04 日 19:26>
      %u 当前包含日期的未激活的 timestamp,如: [2018-03-04 日]
      %U 当前包含日期和时间的未激活的 timestamp,如: [2018-03-04 日 19:26]
      %^t 类似 %t,但是弹出日历让用户选择日期
      %^T 类似 %T,但是弹出日历让用户选择日期和时间
      %^u 类似 %u,但是弹出日历让用户选择日期
      %^U 类似 %U,但是弹出日历让用户选择日期和时间

      注: 激活(active)和未激活(inactive)的 timestamp 的区别在于,后者不会出现在 agenda 中 —— 所以如果是新建一个 headline 到 org-agenda-files 中并且不希望它出现在 agenda 列表中时,应当使用未激活的 timestamp。

    • 剪贴板相关

      标记 描述
      %c 当前 kill ring 中的第一条内容
      %x 当前系统剪贴板中的内容
      %^C 交互式地选择 kill ring 或剪贴板中的内容
      %^L 类似 %^C,但是将选中的内容作为链接插入
    • 标签相关

      标记 描述
      %^g 交互式地输入标签,并用 target 所在文件中的标签进行补全
      %^G 类似 %^g,但用所有 org-agenda-files 涉及文件中的标签进行补全
    • 文件相关

      标记 描述
      %[file] 插入文件 file 中的内容
      %f 执行 org-capture 时当前 buffer 对应的文件名
      %F 类似 %f,但输入该文件的绝对路径
    • 任务相关

      标记 描述
      %k 当前在计时的任务的标题
      %K 当前在计时的任务的链接
    • 外部链接的信息

      这里的链接不仅仅指如 http://www.google.com 这样的网页链接,还包括文件、邮箱、新闻组、IRC 会话等,详情见 Org mode 手册的 External links 一节。

      当然在 capture 里我们用不到所有类型的外部链接,从文档docstring 来看,在 capture 里能用的外部链接只有下面几种

      link type description
      bbdb BBDB 联系人数据库记录链接
      irc IRC 会话链接
      vm View Mail 邮件阅读器中的消息、目录链接
      wl Wunder Lust 邮件/新闻阅读器中的消息、目录链接
      mh MH-E 邮件用户代理中的消息、目录链接
      mew MEW 邮件阅读器中的消息链接
      rmail Emacs 的默认邮件阅读器 Rmail 中的消息链接
      gnus GNUS 邮件/新闻阅读器中的群组、消息等资源链接
      eww/w3/w3m 在eww/w3/w3m 中存储的网页链接
      calendar 日历链接
      org-protocol 遵循 org-protocol 协议的外部应用链接

      注: 文档的内容来自 org-mode 仓库 中的 doc/org.texi,从 commit 历史来看,可能是过时的;但奇怪的是 org-protocol 明明是支持的,docstring 里却完全没有提及……

      这些外部链接,大部分都会在 Emacs 中通过 org-store-link-pros 记录起来,其中会包含这些链接的各个属性,而在 capture 的模板里面,就支持以 %:keyword 的形式来访问这些属性,比如 vm/wl/mh/mew/rmail/gnus 消息中的发件人名称、发件人地址之类的。因为邮件阅读器这块我个人不怎么用,需要详细了解的请查阅文档,而 calendar 完全可以用前面的「时间、日期相关」中的 %t、%T 等标记来替代,因此这里只详细说一下 eww 和 org-protocol。

      eww 可用的特殊标记有如下三个

      标记 描述
      %:type 固定值,eww
      %:link 页面的链接
      %:description 页面的标题,如无则为页面的链接

      org-protocol 可用的特殊标记有如下六个

      标记 描述
      %:type 链接的类型,如 http/https/ftp 等
      %:link 链接地址,在 org-protocol 里的 url 字段
      %:description 链接的标题,在 org-protocol 里的 title 字段
      %:annotation 靠 url 和 title 完成的 org 格式的链接
      %:initial 链接上选中的文本,在 org-protocol 里的 body 字段
      %:query org-protocol 上除掉开头和子协议部分的剩下部分

      此外,在内容模板中还支持自定义函数来插入内容,以 %(sexp) 的形式,比如说我们可以自己写一个 get-current-time 函数来插入当前的时间,那么内容模板可以是这个样子的

      "%(get-current-time)"
      

      而在内容模板中使用自定义函数时,可以将上面 eww 和 org-protocol 的这些特殊标记作为函数的参数,比如一个场景是,用 org-protocol 捕获的网页 title 中包含中括号,会导致下面这样的内容模板出错

      "[[%:link][%:description]]"
      

      这个时候可以定一个一个函数来将 %:description 中的中括号替换成下划线

      (defun replace-bracket-in-title (title)
        ;; blablabla
        )
      

      那么上面那个内容模板可以改成这样

      "[[%:link][%(replace-bracket-in-title \"%:description\")]]"
      
    • 其他

      还有一些特殊标记,不太好归类,就在这里罗列一下。

      "%i" 可以插入一段初始化内容,通常是 org-store-link-plist 中 "initial" 属性的值;如果没有的话,会使用当前 buffer 中被选中的内容;都没有的话就什么也不插入。

      "%^{prop}p" 会提示输入内容,这将会在新增内容中插入一个 property 到 target 中,并且这个 property 的名字是 prop,值则是我们输入的文本。

      "%^{prompt}" 则会用 prompt 作为提示符要求我们输入,并且用我们输入的文本替换模板中相应的内容。比如说 "%{姓名}" 会用 "姓名" 作为提示符要求输入。当有多个标记时,可以用 "%\N" 来插入第 N 个提示输入标记产生的内容,举个例子,下面的内容模板

      "- name: %^{姓名}\n- age: %^{年龄}\n\n%\\1的年龄是%\\2"
      

      (注: 此处的反斜线「\」需要转义,否则「\1」会被视作值为 1 的 ASCII 码特殊字符,感谢 Emacs China 网友 slack-py 指出该问题)

      会要求我们输入姓名和年龄,假如我们输入姓名是 "张三",年龄是 "25",那么最后得到的内容是

      - name: 张三
      - age: 25
      
      张三的年龄是25
      

      "%?" 是一个更特殊的标记,它不会产生任何内容,当所有其他的特殊标记都展开完毕或者输入完毕后,光标将会停留在这个标记所在的位置。

capture 模板示例

所有的 capture 模板都应当以 list 的形式记录在变量 org-capture-templates 中,下面的示例可能会存在模板的 key 冲突的情况,请根据自己的情况来选用或参考示例。

在开始之前,我们先将 org-capture-templates 设置为空

(setq org-capture-templates nil)

用 org-capture 来做任务管理

GTD 一般会有一整套系统的设计,这里只讲一下最一般的新建任务的做法,下面是一个新建书籍阅读任务的示例

(add-to-list 'org-capture-templates
             '("r" "Book Reading Task" entry
               (file+olp "~/Dropbox/org/task.org" "Reading" "Book")
               "* TODO %^{书名}\n%u\n%a\n" :clock-in t :clock-resume t))

上面的两个属性 ":clock-in" 和 ":clock-resume" 在之前没有讲过,是用来对新建内容的行为做一些设置的,不影响内容本身。可用的这些属性一共有 14 个,这里及后面只对涉及到的做说明,其他的请查阅文档

":clock-in" 设置为 t 的时候,会在新建内容时开始计时,这在 GTD 这种场景下是挺有用的。但有可能我们在新建内容时,本来就有一个任务在计时,这种情况下原来的计时会中断掉,这个时候将 ":clock-resume" 设置为 t,可以在新任务完成后,自动恢复原来任务的计时状态。

有些时候我们会对我们需要做的任务做分类,比如上面有一个阅读任务,可能还有工作任务、写作任务,这个时候我们可以利用前面说到的模板分组来更好地进行管理。

在做模板分组前,我们的 org-capture 的任务模板可能是这样的

(add-to-list 'org-capture-templates
             '("r" "Book Reading Task" entry
               (file+olp "~/Dropbox/org/task.org" "Reading" "Book")
               "* TODO %^{书名}\n%u\n%a\n" :clock-in t :clock-resume t))
(add-to-list 'org-capture-templates
             '("w" "Work Task" entry
               (file+headline "~/Dropbox/org/task.org" "Work")
               "* TODO %^{任务名}\n%u\n%a\n" :clock-in t :clock-resume t))

在这个基础上,假设我们要添加一个模板,用来记录从网页上收集的资源、文章的时候,遵循使用描述中关键词的首字母作为选择键的原则,我会希望新建这样一个 capture 模板

(add-to-list 'org-capture-templates
             '("w" "Web Collections" entry
               (file+headline "~/Dropbox/org/inbox.org" "Web")
               "* %U %:annotation\n\n%:initial\n\n%?"))

但这个时候的模板选择键 "w" 和之前任务里的 "Work Task" 就冲突了,为了解决冲突,我只好在其中一个使用小写的 "w" 字母而在另外一个中使用大写的字母 "W"。当我们的模板数量更多时,这种 capture 模板选择键冲突的情况可能会更多。

虽然并不是非常大的问题,但使用模板分组,能尽量地减少这种情况,让我们的模板更加清爽一些

上述情况,我们可以将任务相关的 capture 模板分到一组里,如下所示:

(add-to-list 'org-capture-templates '("t" "Tasks"))
(add-to-list 'org-capture-templates
             '("tr" "Book Reading Task" entry
               (file+olp "~/Dropbox/org/task.org" "Reading" "Book")
               "* TODO %^{书名}\n%u\n%a\n" :clock-in t :clock-resume t))
(add-to-list 'org-capture-templates
             '("tw" "Work Task" entry
               (file+headline "~/Dropbox/org/task.org" "Work")
               "* TODO %^{任务名}\n%u\n%a\n" :clock-in t :clock-resume t))

和前面未分组的模板,有两个不同

  • 多了一个只有 key 和 description 而没有 entry/target/template 的 capture 模板,也就是

    (add-to-list 'org-capture-templates '("t" "Tasks"))
    

    这个模板至关重要,它设定了一组模板的名称和选择键前缀。如果缺失了这个模板,那么后面两个模板是不会起作用的。

  • 原来的两个任务模板,其选择键多了一个前缀 "t"

一图胜千言,在建立分组前,执行 M-x org-capture 时,弹出的模板选择 buffer 是这个样子的

org-capture-buffer-no-group.png

建立分组后,在模板选择 buffer 里看到的是这个样子

org-capture-buffer-group-1.png

这里只会显示模板的 group 的 key 和 description,等我们按下 t 后才会出来组内所有模板的列表

org-capture-buffer-group-2.png

用 org-capture 来记录日志

这个之前也提到过,就是用来做日志记录、日记写作一类的事情,新增的内容和过去的内容都按时间顺序排列,方便我们进行回顾。

做日志记录时,比较推荐用 file+datetree 或者 file+weektree 这两个 target type,当然也不是绝对的,比如说下面这个 capture 模板也是满足基本要求的

(add-to-list 'org-capture-templates
             '("j" "Journal" entry (file "~/Dropbox/org/journal.org")
               "* %U - %^{heading}\n  %?"))

上述模板在每次执行后,在 journal.org 的尾部插入下面这样的内容

* [2018-03-24 六 21:42] - 某件事情的记录

  具体的记录 blablabla

就是如果想要快速地找到某一天或者某一个月的记录,会稍微费力一点,使用 file+datetree 的话,新增的记录会按照「年-月-日」的层次组织起来;而使用 file+weektree 的话,新增的记录会按「年-周-日」的层次组织,下图是两者的对比

org-capture-datetree-and-weektree.png

我个人目前是使用 file+datetree 的。

用 org-capture 收集灵感、记录笔记

这类模板也比较简单,基本上用 file+headline 的 target,然后视情况而定预先设置 tag 什么的。

我个人有一个 capture 模板,用来快速记录未归类的东西,然后会在后面使用 refile 来将这些东西迁移到任务或者笔记中

(add-to-list 'org-capture-templates
             '("i" "Inbox" entry (file "~/Dropbox/org/inbox.org")
               "* %U - %^{heading} %^g\n %?\n"))

笔记则用另外一个 capture 模板

(add-to-list 'org-capture-templates
             '("n" "Notes" entry (file "~/Dropbox/org/notes/inbox.org")
               "* %^{heading} %t %^g\n  %?\n"))

可以看到两个模板其实差不多,无非就是写入的文件不一样。实际上的不同之处在于,在我的笔记本 ~/Dropbox/org/notes/inbox.org 中,我设置了一些文件级别的 tag,如下所示

#+TITLE: 笔记本
#+STARTUP: hideall
#+TAGS: [coding: shell python]
#+TAGS: [shell: grep tail sed ssh]
#+TAGS: [python: ipython pandas numpy]

这样在特殊标记 %^g 展开的时候,就可以用上面设置的 tag 进行补全。

用 org-capture 记录账单

在 Org mode 中利用表格来记录账单是非常合适的一个方式,记录好后利用表格公式(见我的上一篇文章)可以很方便地进行计算、绘图什么的。

下面是我用来记录账单的 capture 模板,利用自定义的函数,来将同一个月的支出记录在同一张表里

(add-to-list 'org-capture-templates
             '("b" "Billing" plain
               (file+function "~/Dropbox/org/billing.org" find-month-tree)
               " | %U | %^{类别} | %^{描述} | %^{金额} |" :kill-buffer t))))

其中的函数 find-month-tree,用来做类似 file+datetree 的事情,不过层级结构只到月为止,其实现如下

(defun get-year-and-month ()
  (list (format-time-string "%Y年") (format-time-string "%m月")))


(defun find-month-tree ()
  (let* ((path (get-year-and-month))
         (level 1)
         end)
    (unless (derived-mode-p 'org-mode)
      (error "Target buffer \"%s\" should be in Org mode" (current-buffer)))
    (goto-char (point-min))             ;移动到 buffer 的开始位置
    ;; 先定位表示年份的 headline,再定位表示月份的 headline
    (dolist (heading path)
      (let ((re (format org-complex-heading-regexp-format
                        (regexp-quote heading)))
            (cnt 0))
        (if (re-search-forward re end t)
            (goto-char (point-at-bol))  ;如果找到了 headline 就移动到对应的位置
          (progn                        ;否则就新建一个 headline
            (or (bolp) (insert "\n"))
            (if (/= (point) (point-min)) (org-end-of-subtree t t))
            (insert (make-string level ?*) " " heading "\n"))))
      (setq level (1+ level))
      (setq end (save-excursion (org-end-of-subtree t t))))
    (org-end-of-subtree)))

效果如下图所示

org-capture-billing.gif

用 org-capture 来记录联系人信息

联系人会有一些常见的属性比如姓名、手机号、邮箱、住址之类的,简单起见可以用表格来做,比如

(add-to-list 'org-capture-templates
             '("c" "Contacts" table-line (file "~/Dropbox/org/contacts.org")
               "| %U | %^{姓名} | %^{手机号}| %^{邮箱} |"))

不过在一些场景下用表格来做可能会不太方便,比如说我们想对联系人进行一些细致的描述之类的,这种情况下一个表格行太长就不太方便了。

因此因外一个方案是,将每个联系人的信息记录为一个 headline 中,联系人的具体属性作为 headline 的 property 进行记录,如果要进行什么描述说明的话就作为 headline 下属的内容就好,如下所示:

(add-to-list 'org-capture-templates
             '("c" "Contacts" entry (file "~/Dropbox/org/contacts.org")
               "* %^{姓名} %^{手机号}p %^{邮箱}p %^{住址}p\n\n  %?" :empty-lines 1))

用 org-capture 来管理密码

和记录联系人类似,如果只是单纯地记录密码,那么可以直接用表格。不过作为一个密码管理方案,我们可能要考虑以下事情

  • org 文件本质上是文本文件,怎么保证密码的安全性?
  • 当我需要新建密码时,是否能在 org-capture 中直接来生成密码?

以上两点都是可以解决的。安全方面,Org mode 支持对文件、headline、headline 中正文等不同层级的加密,详情见 Encrypting org Files.,这里只讲最简单的文件级加密。

首先我们在 Emacs 中先新建好一个后缀为 cpt 的 org 文件,比如 passwords.org.cpt —— 在一个正常的 org 文件后再附加上 cpt 这个后缀,就会被当作一个加密文件,在创建这个文件的时候会要求我们输入加密用的密码,我们只需要把这个主密码记住就好了。

然后我们要写一个函数来要求输入密码,当输入密码为空时,我们就自动生成一个密码 —— 简单起见这里限定生成的密码长度是 16 位,只用字母和数字组成,如下所示。

(defun random-alphanum ()
  (let* ((charset "abcdefghijklmnopqrstuvwxyz0123456789")
         (x (random 36)))
    (char-to-string (elt charset x))))

(defun create-password ()
  (let ((value ""))
    (dotimes (number 16 value)
      (setq value (concat value (random-alphanum))))))


(defun get-or-create-password ()
  (setq password (read-string "Password: "))
  (if (string= password "")
      (create-password)
    password))

然后新建一个模板如下就可以了。

(add-to-list 'org-capture-templates
             '("p" "Passwords" entry (file "~/Dropbox/org/passwords.org.cpt")
               "* %U - %^{title} %^G\n\n  - 用户名: %^{用户名}\n  - 密码: %(get-or-create-password)"
               :empty-lines 1 :kill-buffer t))

用 org-capture 来新建博客文章

我是直接使用 Org mode 的原生支持的 project 来写博客的,写好后将整个 project 导出成 html,放置到配置好的 jekyll 目录下。Org mode 的 project 要求设置一个目录,这个目录下的 org 文件都会被当作 project 中的文章,比如说我的博客对应的 project 设置是这样的

(setq org-publish-project-alist
      '(("blog-org"
         :base-directory "~/Dropbox/org/blog/"
         :base-extension "org"
         :publishing-directory "~/Projects/github-pages/"
         :recursive t
         :htmlized-source t
         :section-numbers nil
         :publishing-function org-html-publish-to-html
         :headline-levels 4
         :html-extension "html"
         :body-only t     ; Only export section between <body> </body>
         :table-of-contents nil
         )
        ("blog-static"
         :base-directory "~/Dropbox/org/blog/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|php"
         :publishing-directory "~/Projects/github-pages/"
         :recursive t
         :publishing-function org-publish-attachment
         )
        ("blog" :components ("blog-org" "blog-static"))))

那么我每次新写文章的时候,就需要用 C-x C-f(find-file) 去在这个目录下新建一个文件,这个过程,是可以用 org-capture 来优化的。

为了方便示例我们把问题简化一下,我需要的是执行 org-capture 后,自动在一个固定的目录下,产生一个命名类似 2018-03-25.org 的文件,并在文件中写入一些固定的内容。

用 org-capture 我们可以这么做

(add-to-list 'org-capture-templates
             `("b" "Blog" plain (file ,(concat "~/Dropbox/org/blog/"
                                               (format-time-string "%Y-%m-%d.org")))
               ,(concat "#+startup: showall\n"
                        "#+options: toc:nil\n"
                        "#+begin_export html\n"
                        "---\n"
                        "layout     : post\n"
                        "title      : %^{标题}\n"
                        "categories : %^{类别}\n"
                        "tags       : %^{标签}\n"
                        "---\n"
                        "#+end_export\n"
                        "#+TOC: headlines 2\n")))

注意,这里和前面的模板有一些不同

  • `("b" "Blog" 这里开头的符号不是单引号
  • target 和 template 两部分中有一个 concat 函数的调用,在其前面有一个逗号

这里涉及到 emacs-lisp 的一些语法细节,想要详细了解的可以查看相关文档: Backquote - GNU Emacs Lisp Reference Manual

用 org-capture 来做网页内容收集

结合 org-protocol,我们可以在外部程序中发送数据到 Emacs 中并触发 org-capture,是非常方便的一个功能。

由于 org-protocol 本身还有很多细节,展开来讲的话内容会很多,这里就只重点讲一下和 org-capture 相关的部分。

首先我们要知道 org-protocol 其实是定义了一个类似通信协议一样的东西,因此我们需要启动 Emacs server 来让外部程序可以访问,在配置文件中加入下面这行配置即可

(server-start)

要启用 org-protocol 的话,还需要在 Emacs 之外做一些设置,本文不准备在这里做过多说明,详情可以参考 org-capture-extension 这个项目,里面对 Linux/OSX/Windows 三个操作系统上的设置都做了详细说明。

在 Emacs 中我们需要加载一下 org-protocol

(require 'org-protocol)

当用 org-protocol 触发 org-capture 时,它会设置 org-store-link-plist 这个变量,根据外部传入的数据设置其中的一些属性。从 org-protocol-do-capture 这个函数的源代码中,我们可以发现这么一段

(org-store-link-props :type type
                      :link url
                      :description title
                      :annotation orglink
                      :initial region
                      :query parts)

也就是说,在 org-store-link-plist 中的属性有六个,分别如下

属性 描述
type 链接的类型,如 http/https/ftp 等,是靠正则 (string-match "^\\([a-z]+\\):" url) 解析出来的
link 链接地址,在 org-protocol 里的 url 字段
description 链接的标题,在 org-protocol 里的 title 字段
annotation 靠 link 和 description 完成的 org 格式的链接
initial 链接上选中的文本,在 org-protocol 里的 body 字段
query org-protocol 上除掉开头和子协议部分的剩下部分

这和我们前面「capture 模板的五个部分」中提到的 org-protocol 在内容模板中可用的六个特殊标记,是一一对应的。

利用这六个属性及对应的六个特殊标记,我们就可以方便地做网页内容的收集了。

我们先为 org-protocol 相关的 capture 模板设立一个分组

(add-to-list 'org-capture-templates '("p" "Protocol"))

最简单的情况是用 org-capture 来做网页书签管理,相应的 capture 模板会比较简单,只需要记录下网页的标题和链接即可,如下所示:

(add-to-list 'org-capture-templates
             '("pb" "Protocol Bookmarks" entry
               (file+headline "~/Dropbox/org/web.org" "Bookmarks")
               "* %U - %:annotation" :immediate-finish t :kill-buffer t))

再进一步的,我们可以选中网页上的内容,通过 org-protocol 和 org-capture 快速记录到笔记中

(add-to-list 'org-capture-templates
             '("pn" "Protocol Bookmarks" entry
               (file+headline "~/Dropbox/org/web.org" "Notes")
               "* %U - %:annotation %^g\n\n  %?" :empty-lines 1 :kill-buffer t))

当然,上面的 capture 模板会有一个问题,假如说一个网页上,有多处我觉得有价值的内容,我都选中了然后通过 org-protocol 调用了 org-capture,那么实际上是会产生多条记录的。这种情况如果能将同一个网页的内容都按顺序放置到同一个 headline 里面,显然是更加合理的。对上面的 capture 模板稍作调整,得到的下面的模板就能满足这个需求:

(add-to-list 'org-capture-templates
             '("pa" "Protocol Annotation" plain
               (file+function "~/Dropbox/org/web.org" org-capture-template-goto-link)
               "  %U - %?\n\n  %:initial" :empty-lines 1))

这里用了 file+function,函数 org-capture-template-goto-link 的定义参考了 reddit 上的这篇帖子

(defun org-capture-template-goto-link ()
  (org-capture-put :target (list 'file+headline
                                 (nth 1 (org-capture-get :target))
                                 (org-capture-get :annotation)))
  (org-capture-put-target-region-and-position)
  (widen)
  (let ((hd (nth 2 (org-capture-get :target))))
    (goto-char (point-min))
    (if (re-search-forward
         (format org-complex-heading-regexp-format (regexp-quote hd)) nil t)
        (org-end-of-subtree)
      (goto-char (point-max))
      (or (bolp) (insert "\n"))
      (insert "* " hd "\n"))))

此外,结合 abo-abo 的 orca 工具,我们还可以针对不同的网站域名,来自动地将网页收集内容进行归类,可以应用的场景有

  • 在 arxiv、google scholar 上用 org-protocol 触发 org-capture 时,自动新增内容到记录待读论文列表的 papers.org 中
  • 在淘宝、京东、亚马逊网站上用 org-protocol 触发 org-capture 时,自动新增内容到记录心愿单列表的 wishlist.org 中

而结合 org-protocol-capture-html 这个工具,我们可以在用 org-protocol 触发 org-capture 时,将网页内容全文转换成 org 文件存储到特定目录中,打造一个类似稍后阅读的工具。

用 org-capture 来新建 Anki 卡片

利用 anki-editor,我们可以在 org 文件中创建卡片并同步到 Anki 软件中。这里就以 anki-editor 中的卡片结构,来展示如何用 org-capture 创建 Anki 卡片。

最简单的例子,是新建单词卡,用来辅助记忆我们学习到的一些新的单词。那么对应的 capture 模板是这个样子的:

(add-to-list 'org-capture-templates
             `("v" "Vocabulary" entry
               (file+headline "~/Dropbox/org/anki.org" "Vocabulary")
               ,(concat "* %^{heading} :note:\n"
                        "%(generate-anki-note-body)\n")))

其中的 generate-anki-note-body 函数如下

(defun generate-anki-note-body ()
  (interactive)
  (message "Fetching note types...")
  (let ((note-types (sort (anki-editor-note-types) #'string-lessp))
        (decks (sort (anki-editor-deck-names) #'string-lessp))
        deck note-type fields)
    (setq deck (completing-read "Choose a deck: " decks))
    (setq note-type (completing-read "Choose a note type: " note-types))
    (message "Fetching note fields...")
    (setq fields (anki-editor--anki-connect-invoke-result "modelFieldNames" `((modelName . ,note-type))))
    (concat "  :PROPERTIES:\n"
            "  :ANKI_DECK: " deck "\n"
            "  :ANKI_NOTE_TYPE: " note-type "\n"
            "  :END:\n\n"
            (mapconcat (lambda (str) (concat "** " str))
                       fields
                       "\n\n"))))

这个函数的定义是抄了 anki-editor(version:20180729) 中的代码,做了一些修改得到的。