普通视图

发现新文章,点击刷新页面。
昨天以前首页

我的生活记录经验及个人工具与方法

2021年9月25日 08:00

中学的时候,我开始写日记,记录琐事和当时再正常不过的一些小心情,就这么一写一直写纸质日记到 2020 年。工作之后,我把上学时候写的日记也带在了身边,不过中学时候的只剩了高三复读时候那一本,早先的记得应该是被我以一些现在看来蠢蠢的理由烧掉了。偶尔兴起会翻一翻自己的日记,重温当时的一些感动,记得有一年翻日记翻到和大学同学的事情,觉得特别感动,没头没脑地给同学发了条信息说「想起当时的XXX觉得好温暖啊」,对方大概会觉得莫名其妙吧。

diary.png

图1  我的纸质日记

我经常想起曾经朋友见到后会说的两句话,一句是“你变了”另外一句则是“你一点都没变”,每次听到这两句话,都特别想问问,对方看到我没变的东西是什么、变了的东西又是什么呢?当然,有些人我可能永远也没有机会去问这个问题了,但幸好我还能从我的日记里,大致地知道自己发生了怎么样的变化,而不是只靠回忆里那些印象深刻但寥寥无几的事件来知道自己是个怎么样的人。当然,在开始写日记的那些年里,我并未意识到这些价值,只是惯性和表达欲使然,再加上潜意识里觉得记忆不靠谱产生了把一些有价值的事情记录下来的想法吧。真正发现记录的深刻价值是在我工作后。开始工作后,我发现自己有很多东西要学、有很多事情要做,每件事情又充满了不确定性和意外,且有时候极其复杂,于是开始在本子上写工作日志 —— 说是工作日志,其实也没有什么章法,写得也很潦草。据说招我进去的领导特别喜欢我学习和记录时候的认真劲,不过工资太少了最后我还是跳槽了。

pachira_journal.jpg

图2  一篇调试错误的工作日志

从 2015 年第二份工作起,我便开始在电脑上记工作日志,简单做了下统计,去掉空行和空格后,从 2015 年 3 月 10 日到今天(2021 年 9 月 25 日),累计写了 15.3 万行共 734 万字,平均每天写 64 行共 3070 字,也不算很多,但是能一做就是这么多年,我还是挺自得的。我的工作日志按日期来顺序记录,都是尽量追求把处理问题的完整过程(包括思考和执行)都记录下来,可复现性还是比较高的,现在同事如果问一个几年前我经手过的事情,我基本上都能在日志中找到过程,如果有必要的环境把当时做的事情重做一遍基本也可以做到 —— 事实上我自己在工作中隔了半个月甚至几个月要重新做一件事情也并不少见,这种情况下工作日志也帮到我很多。

2017_work_journal_sample.png

图3  2017年工作日志片段

到 2018 年下半年的时候,我开始关注自己的时间使用情况,用 Life Cycle 这款 iOS 软件对自己的日常时间开销做一些粗粒度的自动记录,并且在年终总结(见2018年总结2019年总结)的时候用来做一些简单统计和分析。说实在的,这个记录的粒度非常粗,大概就是根据我的位置之类的,判断我在什么场所做什么事情,如果我在一个场所做了两类不同的事情,就需要我自己去手工修正类别,但因为它自动记录和分类的特性,作为一个初步的尝试还是不错的,用了几年后,至少对自己的时间使用情况有了一个数字上的认识。

life_cycle.jpg

图4  "Life Cycle 记录"

如果说写工作日志是为了更好的工作,那么写个人日记和做时间记录则是为了更好地认识自己,而 Life Cycle 这样粗粒度的记录并不能满足我的要求,于是在 2019 年下半年开始,我开始摸索更适合自己的时间记录方式,并在 2020 年逐步进行完善后开始正式使用,目前基本上能把我每天的时间使用情况按事情精确到分钟级进行记录,虽然不是自动化的,但也不会耗费太多精力,写今年的半年总结的时候就用到了这个方法产生的数据。

对于我的记录方法,有若干网友在我的数篇文章的评论里表示感兴趣,所以在这里简单说一下,其实并没有什么很复杂的东西,就是用 org-mode 的 org-agenda 功能,下面是我的 agenda 视图:

my_agenda_view.png

图5  我的 agenda 视图

在这个视图里,我可以按一下 I(大写) 开始某项任务的计时,按一下 O(大写) 则结束任务的计时,操作是很简单的,我要做的是调整一些短期任务让他们显示到「今日事项」这个区域里,这种短期任务一天不会超过十件,只需要在每天做梳理和计划的时候处理即可,而这个梳理和计划我又是通过图上「写今日计划」来进行的,所以这个梳理和计划的耗时也可以得到记录。用 org-mode 来管理任务,这个对 org-mode 用户是很自然的事情,我做的一点点微小创新(其实是不是创新都不好说),就是把一些日常的事情作为“纯计时任务”纳入到这个体系里面了,就这么简单。公司有同事看到我这个方法后有和我讨论,他不用 Emacs 更别说 org-mode 了,但其实是可以借鉴思路的,要能把这个记录过程进行下去,最根本的一点就是操作要尽量的简单和快捷:(1)任务的查找和定位要快,org-agenda 的好处就是提供了一个统一的视图来展示不同来源的任务,其他工具如果也能做到这点的话也会很棒;(2)任务的计时操作要简单便捷,最好是按一个键就能完成。基于这个共识,我们讨论了几种可能的方案:(1)使用 Alfred,自己写点 Apple Script 来定位任务、进行计时;(2)使用 zsh,通过自动补全功能来迅速定位任务并进行计时。

如果对我用 org-agenda 进行时间记录的方法有兴趣,可以看看我准备的示例配置:https://github.com/Linusp/org-agenda-example

需要强调的是,对我来说,自我记录和自我管理,是两个完全不同的事情,我绝对不会想要把这两个事情混到一起去 —— 记录强调时序、强调过去、强调忠于事实,而管理则尝试控制未来的走向,这对我来说太难了,而且我也不认为人能要求事情的未来按照自己的要求或期望进行。我们没有办法减少未来的不确定性,只能去应对它们,但通过记录来减少对过去的记忆的模糊,是每一个人都能去做的。

使用 org-roam 构建自己的知识网络

2020年6月27日 08:00

前言

最近 Roam Research 一类的以网状结构来关联笔记、并以 backlink 的形式来展现笔记上下文的工具非常热门。所谓网状结构,是认为知识和知识是互相关联的,并通过这种互联形成复杂的网络,就像我们的大脑一样;所谓 backlink,是指对单条笔记,展示出链接到这条笔记的其他笔记,这样有助于更好地理解这条笔记的意义。本质上,网状结构和 backlink 其实是一回事,说的都是知识之间的互相链接,不过网状结构着眼于整体结构,而 backlink 则呈现局部形态。

Roam Research 这类工具中的理念,叫做卡片盒笔记法,本文无意对这一想法做过多介绍,如果想进一步了解,可以参考下列文章:

在 Emacs 中,很早就有一个工具 org-brain,想要以 org-mode 为基础让人能建立自己的知识网络,本质上思想是类似的,但交互并不算特别友好,所以我以前稍微用了下就没有继续下去了,而最近出现的 Org-roam 则对标 Roam Research,实现了非常友好的交互,并提供了 org-roam-server 这样非常棒的知识网络可视化界面,经过短时间的使用后,我推荐所有使用 org-mode 来记录自己笔记的人都用一下 org-roam,理由如下:

  1. org-mode 本来就提供了极其强大的链接能力,可以链接到文件、headline 甚至文件的随便一行,也支持了对大量不同类型的外部链接,而 org-roam 为这种能力提供了友好而高效的交互操作
  2. org-roam 复用了 org-capture 的强大功能,使得我们可以自定义各种笔记模板来更好地表示、呈现知识
  3. org-roam-server 提供的笔记网络可视化界面和 org-roam 深度集成,点击界面上的笔记节点就能在 Emacs 中打开对应的笔记

环境说明

  • 操作系统: Ubuntu 16.04
  • Emacs 版本: GNU Emacs 26.1
  • org-mode 版本: 9.3.7
  • org-roam 版本: 开发版 20200615
  • org-roam-server 版本: 开发版 20200621
  • 浏览器: Firefox/Chrome
  • GIF 录制工具: byzanz-record

安装及初步配置

直接从 MELPA 安装即可

(package-install 'org-roam)
(package-install 'org-roam-server)

安装完成后,首先需要设置 org-roam-directory 指向一个目录,用来存放使用 org-roam 创建的笔记。我把笔记都放在 Dropbox 里,所以设置如下

(setq org-roam-directory "~/Dropbox/org/roam")

然后让 org-roam 在 Emacs 启动后就启用

(add-hook 'after-init-hook 'org-roam-mode)

然后设置并启动 org-roam-server 来监听笔记的变化并进行可视化(见org-roam-server 安装说明

(setq org-roam-server-host "127.0.0.1"
      org-roam-server-port 9090
      org-roam-server-export-inline-images t
      org-roam-server-authenticate nil
      org-roam-server-network-label-truncate t
      org-roam-server-network-label-truncate-length 60
      org-roam-server-network-label-wrap-length 20)
(org-roam-server-mode)

上面的配置生效后,会在本地启动一个网页服务,访问 http://127.0.0.1:9090 ,会看到下面这样的界面:

org-roam-server-web.png

由于刚开始并没有创建笔记,上面只会显示一片空白。

然后启用 org-roam-protocol,用来在笔记可视化网页上和 org-roam-server 通信

(require 'org-roam-protocol)

这个 org-roam-protocol 是使用 org-protocol 实现的,依赖操作系统的相关功能,相关设置参考文档

完成上述设置后,就可以开始体验 org-roam 了,执行 M-x org-roam-find-file 创建一条新的笔记,然后刷新笔记可视化页面,就能看到页面上多了一个新的节点了,如下图所示:

org-roam-new.gif

org-roam 的基本使用

首先来看下 org-roam 的基本功能

函数 功能 备注
org-roam-find-file 打开或新建笔记  
org-roam-capture 新建笔记  
org-roam-insert 插入一个指向其他笔记的链接,如果不存在会新建一个笔记  
org-roam-insert-immediate 类似 org-roam-insert,但新建笔记后不打开这个笔记 需要 org-roam 1.2.1
org-roam 显示 backlink  

核心的功能就这么多,没有太多概念、操作要学习,这也是我推荐大家使用它的原因。其基本工作流也很简单,下面是一个示例:

  1. 打开已有笔记,或新建笔记
    • 使用 org-roam-find-file 来新建一个笔记

      org-roam-new.gif

    • 或者,使用 org-roam-find-file 来打开已有的笔记

      org-roam-open-note.gif

    • 或者,在笔记可视化网页上浏览,点击想要查看或编辑的笔记节点,在 Emacs 中打开这个笔记

      org-roam-open-note-2.gif

  2. 选中笔记内容中的某些关键词,使用 org-roam-insert-immediate,创建新的笔记并链接过去,并继续编辑当前的笔记

    org-roam-insert-immediate.gif

    从图上右侧可以看到产生了一个名为 org-mode 的新节点,并和 Emacs 这个节点关联起来了。

  3. 或者,选中笔记内容中的关键词,使用 org-roam-insert,创建新的笔记并链接过去,同时打开新的笔记进行编辑

    org-roam-insert-new.gif

    从图上右侧可以看到产生了一个名为 calc 的新节点,并和 Emacs 这个节点关联起来了。

  4. 或者,用 org-roam-insert-immediate/org-roam-insert 插入一个指向已有笔记的链接

    org-roam-link-to.gif

    上图和步骤 3 一样执行的是 org-roam-insert,但从图上右侧可以看到,只是已有的两个节点之间产生了一条关联,并没有新的节点产生。

  5. 使用 org-roam 展示笔记的 backlinks

    org-roam-show-backlinks.gif

  6. 用 org-roam-capture 在已有笔记中新增内容

    org-roam-append.gif

    org-roam-capture 也可以用于新建笔记,实际上 org-roam-find-file 的逻辑就是:先检查笔记文件是否存在,如果存在就打开,否则就调用 org-roam-capture 来新建笔记。但 org-roam-capture 除了用于新建笔记文件,还可以快捷地在已有笔记中新增内容,且新增内容时可以利用模板来提高效率,比用 org-roam-find-file 打开笔记文件再手工新增会更高效一些。

  7. 重复上述过程

掌握上述工作流后,剩下的事情就是把自己所学到的东西用 org-roam 来进行记录、整理了。

org-roam 进阶

定制笔记模板

org-roam-find-file/org-roam-capture 新建笔记的时候,会要求我们输入笔记标题,假如我们输入的笔记标题是 "org-roam",那么会在新建这个笔记后发现这个笔记只在第一行把我们输入的标题写上去了,别的什么都没有:

#+title: org-roam

在实际使用中,我们可能会有不同的笔记需求,比如说:当我为一个专业术语记录笔记时,我想写下这个术语所属的领域以及它的含义;当我记录一个观点时,我会想写上这个观点是谁提出来的、论据是什么、我自己是支持还是反对;当我读一篇深度学习的论文时,我要记录这篇论文的相关工作、要解决的问题、使用了什么方法、进行了怎么样的实验……

这些需求用 org-roam 是能够满足的,因为 org-roam 通过 org-roam-capture-templates 这个变量提供了定制笔记模板的能力。

具体来说,默认的笔记模板是这样的

'(("d" "default" plain (function org-roam-capture--get-point)
   "%?"
   :file-name "%<%Y%m%d%H%M%S>-${slug}"
   :head "#+title: ${title}\n"
   :unnarrowed t))

每个模板都由 8 个部分组成,我这里以上面的默认模板来进行说明

模板组成 对应默认模板中的内容 描述
key "d" 用来选择模板的快捷键
description "default" 展示用的模板描述
type plain 新增内容的类型
target (function org-roam-capture–get-point) 新增内容的位置, 不可更改
template "%?" 新增内容的模板
file-name :file-name "%<%Y%m%d%H%M%S>-${slug}" 新增笔记文件的文件名模板
head :head "#+title: ${title}\n" 新增笔记的初始化内容,仅新建时生效
properties :unnarrowed t 新增笔记的其他属性

下面对这 8 个模板元素分别说明一下

  • 用来选择模板的 key:

    对应默认模板里的 "d",一个字符的情况下用来直接选择模板,两个字符的情况下用第一个字符表示模板分组、第二个字符用来选择这个分组下的实际模板。

    下面的配置设置了四个模板,其中第二个 ("g" "group") 用来指明一个模板分组,后面的 "ga" 和 "gb" 是这个模板下的子模板。

    (setq org-roam-capture-templates
          '(
            ("d" "default" plain (function org-roam-capture--get-point)
             "%?"
             :file-name "%<%Y%m%d%H%M%S>-${slug}"
             :head "#+title: ${title}\n#+roam_alias:\n\n")
            ("g" "group")
            ("ga" "Group A" plain (function org-roam-capture--get-point)
             "%?"
             :file-name "%<%Y%m%d%H%M%S>-${slug}"
             :head "#+title: ${title}\n#+roam_alias:\n\n")
            ("gb" "Group B" plain (function org-roam-capture--get-point)
             "%?"
             :file-name "%<%Y%m%d%H%M%S>-${slug}"
             :head "#+title: ${title}\n#+roam_alias:\n\n")))
    

    上面的模板生效后,首先在执行 org-roam-find-file 新建笔记时,就会看到一个选择界面,如下图所示:

    org-roam-capture-select-template.gif

    如果输入 d 会直接打开新建笔记的编辑窗口,如果输入 g 会展开分组模板要求我们再输入一次来选择具体的模板,如下图所示:

    org-roam-capture-select-group-template.gif

  • 用来描述模板的 description:这个元素就是起到单纯的描述作用,没有功能上的意义
  • 用来说明新增内容类型的 type:本来有 plain/entry/item/checkitem/table-line 五种取值,但在 org-roam 中作用都是一样的,建议一律使用 plain
  • 用来说明新增内容位置的 target:这一项在 org-roam 中不可更改
  • 设置新增内容模板的 template:

    这个元素是整个模板中的核心,其中的内容可以分为两类:

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

    对于第一类内容没啥可说的,唯一值得一提的是,如果需要模板是多行的文本,需要在模板中用 "\n" 来指明要换行,如模板内容 "第一行\n第二行" 最后就会在笔记中显示为:

    第一行
    第二行
    

    这里说一下以 % 开头的特殊标记,由于这块内容很多,这里只列举一些常用的供读者参考:

    标记 描述
    %<…> 自定义格式的时间戳,如: %<%Y-%m-%d>,会得到 <2018-03-04 日>
    %t 当前日期,展开后的格式固定为 <2018-03-04 日> 这样
    %T 当前日期和时间,展开后的格式固定为 <2018-03-04 日 19:26> 这样
    %u 当前日期,展开后的格式固定为 [2018-03-04 日] 这样
    %U 当前日期和时间,展开后的格式固定为 [2018-03-04 日 19:26] 这样
    %prompt 用 prompt 作为提示要求我们输入并填充在这个模板元素所在的位置
    %? 其他所有特殊标记填充完毕后,光标将停留在这个元素的位置等待我们编辑
  • 指定新增笔记文件名的 file-name:

    org-roam 中新建笔记一般都是以一个新文件的形式来创建的,支持用 file-name 来设置这个新文件的文件名,默认模板中的这块设置为 "%<%Y%m%d%H%M%S>-${slug}",分为两部分

    • %<%Y%m%d%H%M%S> :参考上一节 template 部分的特殊标记
    • ${slug} :将笔记标题文字做处理后得到的文本,这些处理包括大写字母转小写、去除一些特殊字符等

    这块建议使用默认模板就好。

  • 设置新增笔记初始内容的 head:

    这个设置用来在新建笔记文件时设置初始内容,只会执行一次,也就是说之后如果使用 org-roam-capture 新增内容到已有笔记中时,这个设置的内容是不会再写入到文件中的。

    默认模板这块的内容是 "#+title: ${title}\n",只设置了笔记文件的标题,建议改为如下内容:

    :head "#+title: ${title}\n#+roam_alias: \n#+roam_tags: \n"
    

    这样设置后新建笔记文件的初始内容将会是:

    #+title: 示例标题
    #+roam_alias:
    #+roam_tags:
    

    "#+roam_alias" 可以给这条笔记设置别名,这样在其他笔记中引用的时候会更方便;"#+roam_tags" 可以用来为这条笔记添加标签,使得在用 org-roam-find-file 查找已有笔记时能根据 tag 来进行过滤。

  • 设置新增内容其他属性的 properties:

    这些属性用于对新建笔记内容的行为做一些额外的控制,列举几个常用的:

    • :unnarrowed t: org-roam 推荐的设置,表示现实整个笔记文件,如果不加这个设置,用 org-roam-capture 增加内容到已有笔记文件中时,仅会显示当前我们输入的内容,而不会显示这个笔记文件中已有的内容
    • :empty-lines 1: 在新增的笔记内容前后加一个空行,使用 org-roam-capture 增加内容到已有笔记文件中时比较有用

org-roam 的笔记模板是利用 org-capture 实现的,上述模板元素中 file-name 和 head 是 org-roam 在 org-capture 模板的基础上增加的新元素;其他六个部分,target 在 org-roam 中不可更改,key、description、type 和 template 的更详细说明可以参考我之前写的一篇介绍 org-capture 文章中的相关内容,capture 模板的五个部分

为了能更直观地理解模板的工作机制,这里给几个模板的示例:

  • 用于记录专业术语的模板

    (add-to-list 'org-roam-capture-templates
                 '("t" "Term" plain (function org-roam-capture--get-point)
                   "- 领域: %^{术语所属领域}\n- 释义:"
                   :file-name "%<%Y%m%d%H%M%S>-${slug}"
                   :head "#+title: ${title}\n#+roam_alias:\n#+roam_tags: \n\n"
                   :unnarrowed t
                   ))
    

    将上面的配置拷贝到你的 Emacs 配置中,并置于所有 org-roam 相关配置的后面,就可以在你的 org-roam 中使用这个模板,后面的示例模板也是一样,但要注意不同模板的 key 不要有冲突。

    org-roam-new-term.gif

  • 用于记录论文笔记的模板

    (add-to-list 'org-roam-capture-templates
                 '("p" "Paper Note" plain (function org-roam-capture--get-point)
                   "* 相关工作\n\n%?\n* 观点\n\n* 模型和方法\n\n* 实验\n\n* 结论\n"
                   :file-name "%<%Y%m%d%H%M%S>-${slug}"
                   :head "#+title: ${title}\n#+roam_alias:\n#+roam_tags: \n\n"
                   :unnarrowed t
                   ))
    

    org-roam-new-paper-note.gif

另外,org-roam-insert-immediate 不使用 org-roam-capture-templates,而是使用一个专门的 org-roam-capture-immediate-template 来设置新建内容的模板,且只能有一个模板,所以设置这个模板的配置是这样的(以默认配置为例)

(setq org-roam-capture-immediate-template
      '("d" "default" plain (function org-roam-capture--get-point)
        "%?"
        :file-name "%<%Y%m%d%H%M%S>-${slug}"
        :head "#+title: ${title}\n"
        :unnarrowed t))

实现网页内容摘录

这部分内容需要 org-protocol,后续内容是在 org-protocol 已经设置好的基础上展开的,如果 org-protocol 设置存在问题,请查阅文档,或这评论区留言来讨论。

利用 org-protocol 这样的外部程序和 Emacs 进行通信的机制,我们可以使用 javascript 来抓取网页上的信息发送到 Emacs 中,而 org-roam 也支持了这种机制。在 org-roam 中可以通过 org-roam-capture-ref-templates 来设置网页捕获相关的模板,默认的设置是这样的:

(setq org-roam-capture-ref-templates
      '(("r" "ref" plain (function org-roam-capture--get-point)
         ""
         :file-name "${slug}"
         :head "#+title: ${title}\n#+roam_key: ${ref}\n"
         :unnarrowed t)))

可以看到,模板本身和前面的笔记模板是一样的,没有什么特别。但我们可以创建一个小书签,来利用这个模板,抓取网页标题和链接然后新建一个笔记到 org-roam 中,如下图所示:

org-roam-store-link.gif

上图中小书签的内容来自 org-roam 的文档,具体内容为:

javascript:location.href = 'org-protocol://roam-ref?template=r&ref=' + encodeURIComponent(location.href) + '&title=' + encodeURIComponent(document.title)

添加的方法是在浏览器中新增一个书签,书签的名字随意(上图中我设置为了“网页抓取”),书签的 URL 填上上面的 javascript 代码。下图是 Firefox 中创建这样的小书签的示意图:

create-bookmarklet-in-firefox.gif

这个小书签的内容分成几部分:

  • 第一部分说明小书签要访问的地址,这个就是 org-roam-protocol 的通信地址

    javascript:location.href='org-protocol://roam-ref'
    
  • 第二部分指定要使用的笔记模板,从 org-roam-capture-ref-templates 中匹配

    '?template=r'
    
  • 第三部分获取一些网页的信息,并设置到变量中,供模板填充使用

    '&ref=' + encodeURIComponent(location.href) + '&title=' + encodeURIComponent(document.title)
    

    前面的默认模板中,head 部分内容为

    :head "#+title: ${title}\n#+roam_key: ${ref}\n"
    

    需要填充 "title" 和 "ref" 两个变量,小书签中第三部分内容就是获取了当前网页的链接赋值给 "ref" 变量,并获取网页标题文本赋值给 "title" 变量了,这样这个模板就能自动填充好了。

不过这个模板和小书签过于简单,只能记录网页链接,我设计了一个模板和对应的小书签,可以做到进行网页标注、摘录,效果见下图:

org-roam-annotate-web.gif

要达到上图的效果,首先,在 org-roam-capture-ref-templates 中新增一个模板

(add-to-list 'org-roam-capture-ref-templates
             '("a" "Annotation" plain (function org-roam-capture--get-point)
               "%U ${body}\n"
               :file-name "${slug}"
               :head "#+title: ${title}\n#+roam_key: ${ref}\n#+roam_alias:\n"
               :immediate-finish t
               :unnarrowed t))

然后新建一个小书签,内容为

javascript:location.href = 'org-protocol://roam-ref?template=a&ref=' + encodeURIComponent(location.href) + '&title='+encodeURIComponent(document.title) + '&body='+encodeURIComponent(function(){var html = "";var sel = window.getSelection();if (sel.rangeCount) {var container = document.createElement("div");for (var i = 0, len = sel.rangeCount; i < len; ++i) {container.appendChild(sel.getRangeAt(i).cloneContents());}html = container.innerHTML;}var dataDom = document.createElement('div');dataDom.innerHTML = html;['p', 'h1', 'h2', 'h3', 'h4'].forEach(function(tag, idx){dataDom.querySelectorAll(tag).forEach(function(item, index) {var content = item.innerHTML.trim();if (content.length > 0) {item.innerHTML = content + '&#13;&#10;';}});});return dataDom.innerText.trim();}())

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

2018年2月28日 08:00

本文是《强大的 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) 中的代码,做了一些修改得到的。

Emacs 的 Python3 开发环境配置

2017年9月16日 08:00

基础配置

基础配置就没啥好说的了,就是缩进宽度啦,各种 minor-mode 的添加啦之类的,直接上配置

(defun my-python-mode-config ()
  (setq python-indent-offset 4
        python-indent 4
        indent-tabs-mode nil
        default-tab-width 4

        ;; 设置 run-python 的参数
        python-shell-interpreter "ipython"
        python-shell-interpreter-args "-i"
        python-shell-prompt-regexp "In \\[[0-9]+\\]: "
        python-shell-prompt-output-regexp "Out\\[[0-9]+\\]: "
        python-shell-completion-setup-code "from IPython.core.completerlib import module_completion"
        python-shell-completion-module-string-code "';'.join(module_completion('''%s'''))\n"
        python-shell-completion-string-code "';'.join(get_ipython().Completer.all_completions('''%s'''))\n")

  (add-to-list 'auto-mode-alist '("\\.py\\'" . python-mode))
  (hs-minor-mode t)                     ;开启 hs-minor-mode 以支持代码折叠
  (auto-fill-mode 0)                    ;关闭 auto-fill-mode,拒绝自动折行
  (whitespace-mode t)                   ;开启 whitespace-mode 对制表符和行为空格高亮
  (hl-line-mode t)                      ;开启 hl-line-mode 对当前行进行高亮
  (pretty-symbols-mode t)               ;开启 pretty-symbols-mode 将 lambda 显示成希腊字符 λ
  (set (make-local-variable 'electric-indent-mode) nil)) ;关闭自动缩进

(add-hook 'python-mode-hook 'my-python-mode-config)

另外如果是 Emacs 25.1 的话,有一个已知 bug,会导致执行 run-python 的时候,python shell 里显示一堆乱码,下面的方法能够解决

(setenv "IPY_TEST_SIMPLE_PROMPT" "1")

Emacs + Python3 的问题

从去年开始,因为工作的原因,日常的开发环境从 Python2 切换成了 Python3,一开始还是有一点不太习惯的,其中 Python 本身的语法差异倒真没有带来太多的不适应,一开始的抗拒主要还是因为不少 Python 的库在对 Python3 的支持上多少有点问题。

当然,因为公司是用 Python3 的,碰到上述问题的时候就会去找替代方案了,加上主流的一些库也有了对 Python3 的支持,所以现在已经习惯了用 Python3,而且本身是从事 NLP 相关的工作,读写文本的时候不用每次都 encode/decode,还是挺舒服的。

Python2 还是 Python3 这个就不想讨论了,网上相关的讨论也不少了。我这边的问题主要是,切换成 Python3 后,原来 Python 的配置多少都有点问题,比如语法检查、自动补全等默认都是用系统的 Python 环境的,要处理 Python3 的代码就需要额外做点事情,我这个人实在是懒于是去掉了语法检查、自动补全这些功能,将就着用着最基础的一些功能,倒也不是不能过日子。

有时候也有考虑重新配置一下 Python 环境,但是一看到 elpy 啊 projectile 这些稍微复杂点的 package 就犯懒,倒是这阵子用一些更小的 package 一点一点地加新功能,貌似倒是已经解决了自己的需求了,加上有些时间没写东西了,想着写点东西,顺便分享下踩到的坑吧。

company + jedi-core 的 Python3 配置

首先是自动补全了,一开始是用 auto-complete 的,不过被 auto-complete 坑过太多次了, company 算是后起之秀,配置起来也挺方便的。

把 company 装上

(when (not (require 'company nil :noerror))
  (message "install company now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'company))

然后在启动 Emacs 的时候开启全局的 company-mode

(add-hook 'after-init-hook 'global-company-mode)

company-mode 默认已经配置好了多个语言的 backends,基本上是开箱即用的,查看变量 company-backends 可以看它当前使用的 backends,默认是

(company-bbdb
 company-nxml
 company-css
 company-eclim
 company-semantic
 company-clang
 company-xcode
 company-cmake
 company-capf
 company-files
 (company-dabbrev-code company-gtags company-etags company-keywords)
 company-oddmuse company-dabbrev)

这些应付一些简单的场景足够用了。

然后是安装 company-jedi

(when (not (require 'company-jedi nil :noerror))
  (message "install company-jedi now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'company-jedi))

company-jedi 是 company 的一个 backend,使用 jedi 这个 Python 的自动补全和静态分析工具。需要注意的是,使用 package-install 安装 company-jedi 就好了,它会安装 jedi-core 这个 package,里面有对 jedi 的封装。说这个是因为用户如果没有看 company-jedi 的说明,有可能会去安装 jedi 这个 Emacs package,但实际上这个 package 是一个 auto-complete 的后端,完全不用。

到目前为止的操作都是通用的,和 Python2/Python3 都没有关系,但要知道,jedi 的工作原理是根据一个 Python 环境里的标准库及安装的非标准库来进行补全的,也就是说,它需要依赖一个外部的 Python 环境,如果去看 emacs-jedi 的文档,会看到要求用户执行 jedi:install-server 来建立一个 Python 环境,而这个命令实际上会在 ~/.emacs.d/.python-environments 这个目录下建立一个 virtualenv 环境,默认用的是 Python2.7。

所以如果想为 Python3 配置 jedi,请注意 不要使用 jedi:install-server 这种方式

既然知道了 emacs-jedi 的工作原理,那就好办了,那我就自己在 ~/.emacs.d/.python-environments 这个目录下建立一个 Python3 的 virtualenv 环境呗。

首先建立 ~/.emacs.d/.python-environments/ 这个目录

mkdir -p ~/.emacs.d/.python-environments/

然后在其中创建 virtualenv 环境,下面的示例中为这个 virtualenv 环境命名为 jedi,取别的名字都可以的

cd ~/.emacs.d/.python-environments/
virtualenv -p /usr/bin/python3  --prompt="<venv:jedi>" jedi

然后在这个 virtualenv 环境中安装需要的 Python 依赖,依赖分两部分,一部分是 jedi 相关的几个 Python 包,是自动补全必须的,这些东西都在 jedi-core 这个 Emacs package 里的 setup.py 里写好了,其内容如下

setup(
    name='jediepcserver',
    version='0.2.7',
    py_modules=['jediepcserver'],
    install_requires=[
        "jedi>=0.8.1",
        "epc>=0.0.4",
        "argparse",
    ],
    entry_points={
        'console_scripts': ['jediepcserver = jediepcserver:main'],
    },
    **args
)

可以看到,依赖的是 jedi 和 epc 两个 Python 包,我们可以手动安装它们

~/.emacs.d/.python-environments/jedi/bin/pip install jedi>=0.8.1 epc>=0.0.4 argparse

也可以直接使用这个 setup.py 来安装

~/.emacs.d/.python-environments/jedi/bin/pip install --upgrade ~/.emacs.d/elpa/jedi-core-20170319.2107/

其次是需要用于补全的 Python 的非标准库,比如说我经常用 sklearn、tensorflow 之类的工具,我想在写相关的代码的时候能补全,那么要在我们刚才建立好的 virtualenv 环境里安装好这些 Python 包。

~/.emacs.d/.python-environments/jedi/bin/pip install tensorflow==1.3.0 scipy==0.19.1 numy==1.13.1 scikit-learn==0.19.0

至此外部的设置都已经好了,然后就是要在 Emacs 里设置来使用我们刚才建立好的这个 virtualenv 环境

(setq jedi:environment-root "jedi")
(setq jedi:server-command (jedi:-env-server-command))

然后设置当打开 Python 代码文件的时候,启动 jedi

(defun config/enable-jedi ()
  (add-to-list 'company-backends 'company-jedi))
(add-hook 'python-mode-hook 'jedi:setup)
(add-hook 'python-mode-hook 'config/enable-jedi)

还有一些补全的细节可以设置,如

  • 输入句点符号 "." 的时候自动弹出补全列表,这个主要是方便用来选择 Python package 的子模块或者方法

    (setq jedi:complete-on-dot t)
    
  • 补全时能识别简写,这个是说如果我写了 "import tensorflow as tf" ,那么我再输入 "tf." 的时候能自动补全

    (setq jedi:use-shortcuts t)
    
  • 设置补全时需要的最小字数(默认就是 3)

    (setq compandy-minimum-prefix-length 3)
    
  • 设置弹出的补全列表的外观

    让补全列表里的各项左右对齐

    (setq company-tooltip-align-annotations t)
    

    如果开启这个,那么补全列表会是下面这个样子

    company-aligned-tooltip.png

    默认是这个样子

    company-default-tooltip.png

    补全列表里的项按照使用的频次排序,这样经常使用到的会放在前面,减少按键次数

    (setq company-transformers '(company-sort-by-occurrence))
    

    在弹出的补全列表里移动时可以前后循环,默认如果移动到了最后一个是没有办法再往下移动的

    (setq company-selection-wrap-around t)
    
  • 对默认快捷键做一些修改

    默认使用 M-n 和 M-p 来在补全列表里移动,改成 C-n 和 C-p

    (define-key company-active-map (kbd "M-n") nil)
    (define-key company-active-map (kbd "M-p") nil)
    (define-key company-active-map (kbd "C-n") 'company-select-next)
    (define-key company-active-map (kbd "C-p") 'company-select-previous)
    

    设置让 TAB 也具备相同的功能

    (define-key company-active-map (kbd "TAB") 'company-complete-common-or-cycle)
    (define-key company-active-map (kbd "<tab>") 'company-complete-common-or-cycle)
    (define-key company-active-map (kbd "S-TAB") 'company-select-previous)
    (define-key company-active-map (kbd "<backtab>") 'company-select-previous)
    

结合 virtualenv 来使用 flycheck

首先我们要安装 flycheck 这个实时语法检查工具

(when (not (require 'flycheck nil :noerror))
  (message "install flycheck now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'flycheck))

在 python-mode 里启用也很简单

(defun config/enable-flycheck ()
  (flycheck-mode t))
(add-hook 'python-mode-hook 'config/enable-flycheck)

flycheck 使用 pylint 来对代码进行语法和代码规范的检查,实际上会使用 executable-find 这个方法来确定使用的 pylint

(defcustom flycheck-executable-find #'executable-find
  "Function to search for executables.

The value of this option is a function which is given the name or
path of an executable and shall return the full path to the
executable, or nil if the executable does not exit.

The default is the standard `executable-find' function which
searches `exec-path'.  You can customize this option to search
for checkers in other environments such as bundle or NixOS
sandboxes."
  :group 'flycheck
  :type '(choice (const :tag "Search executables in `exec-path'" executable-find)
                 (function :tag "Search executables with a custom function"))
  :package-version '(flycheck . "0.25")
  :risky t)

而 executable-find 的工作原理是从 exec-path 这个变量里包含的的路径下寻找对应的可执行程序

(defun executable-find (command)
  "Search for COMMAND in `exec-path' and return the absolute file name.
Return nil if COMMAND is not found anywhere in `exec-path'."
  ;; Use 1 rather than file-executable-p to better match the behavior of
  ;; call-process.
  (locate-file command exec-path exec-suffixes 1))

如果只是为了支持 Python3,那么我们可以自己建立一个 Python3 的 virtualenv,然后将其路径加到 exec-path 的最前面

(push "<YOUR PYTHON3 VENV>/bin/" exec-path)

当然记得在里面安装 pylint,不然还是会用系统环境也就是 Python2 环境里的 pylint。

这种方法可以 work,但是会有不方便的地方,比如说我有时候也有可能会写 Python2 代码,遇到 Python3 已经不兼容的语法,上述方法会导致 flycheck 认为是语法错误。另外一个就是,比较良好的开发习惯,是用 virtualenv 隔离开每个项目的依赖,不同项目的同一个依赖可能会版本不一样,这样的话 flycheck 如果只使用静态的环境就没有办法应付。

当然,上一节的自动补全用的是一个统一的 virtualenv 环境,也会有类似的问题,不过要改起来会麻烦一些,所以先略过。

flycheck 的这个问题倒是好解决,既然我每个项目都会有一个独立的 virtualenv,那么能不能做到我打开对应项目的代码的时候就使用对应的 virtualenv 环境呢,比如说将对应的路径添加到 exec-path 这个列表的前面?

答案是可以的,方法是使用 auto-virtualenvwrapper,这个 package 可以根据当前的文件寻找当前目录或者上级目录中的 virtualenv 环境,然后启用。

(when (not (require 'auto-virtualenvwrapper nil :noerror))
  (message "install auto-virtualenvwrapper now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'auto-virtualenvwrapper))

然后设置一下在 python-mode 里启用它

(add-hook 'python-mode-hook #'auto-virtualenvwrapper-activate)

可以做到切换 buffer 的时候自动切换对应的 virtualenv 环境

(add-hook 'window-configuration-change-hook #'auto-virtualenvwrapper-activate)

然后我们要保证 flycheck 会在这个 virtualenv 环境里去寻找 pylint,也就是说,我们要临时修改一下 exec-path 的值

(declare-function python-shell-calculate-exec-path "python")

(defun flycheck-virtualenv-executable-find (executable)
  "Find an EXECUTABLE in the current virtualenv if any."
  (if (bound-and-true-p python-shell-virtualenv-root)
      (let ((exec-path (python-shell-calculate-exec-path)))
        (executable-find executable))
    (executable-find executable)))

(defun flycheck-virtualenv-setup ()
  "Setup Flycheck for the current virtualenv."
  (setq-local flycheck-executable-find #'flycheck-virtualenv-executable-find))

注:上述代码来自lunaryorn 的配置

PEP8 的支持

上面配置好的 flycheck 所做的语法检查和静态分析,对于不符合 PEP8 规范的语句已经会做一些提示了,不过说实话,一些东西我们可能并不想在上面化太多精力,运算符前后一个空格啦、函数之间空两行啦、类内方法之间空一行啦之类的,其实可以靠 py-autopep8 来格式化代码自动完成。

安装相应的 Emacs package

(when (not (require 'py-autopep8 nil :noerror))
  (message "install autopep8 now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'py-autopep8))

当然它其实是使用的 Python 的 autopep8 这个外部工具,所以也需要安装它

pip install autopep8

然后在 python-mode 里启用就好了,下面的配置会让 Emacs 每次在保存 Python 文件的时候自动调用 autopep8 进行格式化

(add-hook 'python-mode-hook 'py-autopep8-enable-on-save)

当然我们也可以额外设置一些参数,比如默认的一个标准是每行最大字符数为 80,如果超过了,格式化的时候会将该行折行。下面的配置可以设置为 100

(setq py-autopep8-options '("--max-line-length=100"))

强大的 Org mode(3): 表格的基本操作及公式、绘图

2016年6月3日 08:00

本文是《强大的 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

Org mode 中的表格

Org mode 原生支持表格,这是 rst 或者 markdown 所不具备的特点。一个表格的例子如下所示:

| Name  | Phone | Age |
|-------+-------+-----|
| Peter |  1234 |  17 |
| Anna  |  4321 |  25 |

表格用 "|" 符号作为列分隔符,而在 Org mode 中,如果某一行文字的 第一个非空白符号 是 "|" 的话,就会被视作是一个表格,而 '-' 符号组成的分隔线之上的部分则被认为是各列的名称(不过列名并不是必须的)。

Org mode 中的表格除了用来展示数据,还支持表格公式,能在表格中原有数据的基础上进行计算;还能直接提供数据给诸如 gnuplot 这样的程序来绘制图像;此外在 source block 中还可以读取指定表格中的数据用于更复杂的计算。

除了表格本身的功能外,基于 Org mode 的日程和任务管理功能,可以对任务完成情况进行定期统计,并将统计结果输出成表格。

表格的创建

利用 "第一个非空白符号是 | 的行被视作表格行" 这个特性,手动创建表格也是非常方便的。首先手动完成第一行,确定表格有多少列,然后按下 TAB 键就能自动在下一行插入新一行表格行了,如下图所示。

org-table-create-1.gif

当然,一个完整的表格一般是有 header (就是表示列名那一行)的,要有 header 的话就需要 "-" 组成的分隔线,有两种方式可以插入分隔线。

一种办法是新起一行,对齐后输入 "|-" ,即一个列分隔附跟一个连字符,如下所示:

| 单元格 | 单元格 | 单元格 |
|-

然后按 TAB 键,就会补全成为这样的样式了

| 单元格 | 单元格 | 单元格 |
|-------+-------+-------|

第二种办法是使用 "C-c -" 这个快捷键来快速插入,如下图所示。

org-table-create-2.gif

除了手动创建表格,还可以通过 "C-c |" 这个快捷键来快速创建指定大小的表格。使用这个快捷键后,会提示输入创建的表格的大小,默认是5x2也就是5列2行的,且其中一行是 header,如下图所示。

org-table-create-3.gif

第三种创建方法是直接将 buffer 上已有的数据格式化成表格,比如如果是以逗号(,)分隔的 CSV 格式的数据,可以将其拷贝到当前在编辑的 Org mode 文档中,选中,然后使用 "C-c |" 这个快捷键,就能将其转换成表格形式,如下图所示。

org-table-create-4.gif

这种方法不会自动插入水平分隔线,所以在完成后,可按自己的需要选择添加或者不添加。

如果数据之间是用空格分隔的,该如何转换呢?选中后使用快捷键"C-u 1 C-c |"即可。

更进一步的,Org mode 提供了 "org-table-import" 这个命令来将外部文件导入到 Org mode 文档中并用它来创建表格,与之对应的,命令 "org-table-export" 则能将Org mode 文档中的表格导出成文件。文件格式可以是 CSV 的,也可以是以制表符(TAB)或空白字符作为分隔符的。

表格的基本操作

下表是表格常用操作的快捷键:

快捷键 说明
TAB 切换到下一个单元格,如已是最后一个单元格,则新建一行并跳到该行第一个单元格
M-S-right 在当前列前插入一列
M-S-left 删除当前列
M-S-down 在当前行前插入一行
M-S-up 删除当前行
C-m 移动到下一行,或新建一行
M-up/M-down 将当前行往上/下移动
M-left/M-right 将当前列往左/右移动
C-c ` 编辑当前单元格
C-c C-x C-w 剪切某个区域的表格内容
C-c C-x C-y 拷贝复制的内容到表格
S-return 当单元格无内容时,将其上方第一个非空内容拷贝过来;否则拷贝当前内容到下一行并随之移动
C-c C-c 强制表格重新排列
C-c ^ 表格排序

这些操作就不再进行图示了,希望读者自行实践。

快捷键注释(以 Linux 系统为例):

  1. M 表示 Alt 键
  2. S 表示 Shift 键
  3. C 表示 Ctrl 键
  4. up/down/left/right 分别表示 上/下/左/右 四个方向键
  5. return 表示回车键

表格公式

Org mode 中的表格的另外一个强大之处,在于它支持公式。在表格区域使用快捷键 "C-c '",就可以对表格公式进行编辑,完成后公式会显示在表格下方,以 "#+TBLFM:" 开头,如下图所示。

org-table-edit-formula.gif

使用 "C-c '" 后能在一个独立的、临时的 buffer 中编辑公式,但我们也可以在表格下方手工添加以 "+TBLFM:" 开头的行,然后直接添加公式。

上面这个公式表示将第四列的值设为第二列的值与第三列的值的乘积。在编辑好公式并保存后,将光标移动到公式所在行然后使用 "C-c C-c",就可以应用公式到表格中。如下图所示:

org-table-formula-eval.gif

在Org mode的表格公式中,用 "@" 来表示行,用 "$" 来表示列,最简单的,"@3$2" 表示的是第三行第二列的位置。使用快捷键 "C-c }" 可以开启表格的横纵坐标显示——若要关闭的话也是用它。如果是用 "C-c '" 来进行公式编辑,在输入表格位置时,会看到表格上对应的位置会在当时高亮,所以建议用这种方式进行编辑。

如果只给一个坐标,则另一个坐标会被设为"当前行"或者"当前列",这在批量处理表格内容时会有用。

如果想表示一个区域的话,用 ".." 来表示。

下面这个表示左上角为第二行第一列单元格、右下角为第四行第三列单元格的区域,共包含 9 个单元格。

@2$1..@4$3

下面这个则表示"当前行"的第一列到第三列的区域:

$1..$3

在公式中,可以用 "@#" 表示当前行的行号,用 "$#" 表示当前列的列号,在一些稍复杂点的公式里会有用。

此外,还可以定义常量、变量,或者给某个单元格命名,然后引用它们。假设其名字为 "name",那么 "$name" 就可以引用它了。常量的定义可以通过 "org-table-formula-constants" 来进行,这样定义的常量是全局的;如果要定义局部的常量,可以在org文件中添加诸如这样的行:

#+CONSTANTS: pi=3.14 eps=2.4e-6

还可以在当前表格引用其他表格的域,这需要其他表格被命名为某个名字,如"FOO",我们要在另一个表格中使用其第三行第四列的域,将其值赋给当前表格的第五行第二列,则可以这样写:

@5$2=remote(FOO, @3$4)

下图将被命名为 "fruit_expend" 的表格的第 6 行第 4 列的数据插入到新表格的第二行第二列中: org-table-formula-example-1.gif

Org mode 的表格公式中,四则运算符都能正常使用,不过略有不同——乘号 "*" 的优先级要比除号 "/" 要高,因此

$3 / $2 * $1

会被解释为

$3 / ($2 * $1)

Org mode 默认使用的是 Emacs 中自带的 Calc 这个 package 来进行计算,而 Calc 中提供了相当丰富的计算方法,这里列举一二:

  1. 基础算术方法: abs, sign, inv, sqrt, min, max,详见 Arithmetic Functions
  2. 对数方法: ln, exp, log,详见 Logarithmic Functions
  3. 三角函数: sin, cos, tahn,详见 Trigonometric/Hyperbolic Functions
  4. 随机数方法: random
  5. 向量/矩阵方法: vunion, vint, vsum, vmean, vmax, vmin, vmedian,详见 Vector/Matrix Functions

Calc 的内容比较多,这里不做深入展开,有需要的话可以参考 GNU Emacs Calc Manual

此外,表格公式还能以 Emacs Lisp 的形式来进行编写,不过要在这种形式的公式前加上单引号 "'",才能正确求值。在 Emacs Lisp 形式的公式表达式中,传入的参数会被当作字符串,所有需要用格式化选项 "N" 来指明参数类型都是数值。如下图,在不加格式化选项时,公式计算出错,加上 ";N" 后才得到了正确的结果。

org-table-formula-with-lisp.gif

所有的格式化选项,必须通过分号 ";" 和公式进行分隔并跟随在公式后面,可用的选项有:

  • p: 设置计算精度
  • n/s/e/f: 设置结果的输出格式
    • n3: 输出结果为3位有效数字(1.45)
    • s3: 输出结果为科学计数法,3位有效数字(1.45e0)
    • e3: 输出结果为工程计数法,3位有效数字(0.145e1)
    • f3: 输出结果精确至小数点后3位
  • D/R: 计算时使用角度制还是弧度制(如三角函数)
  • F/S: 分数还是符号(当为 S 时,若结果不为整数,则显示式子本身,如: sqrt(6))
  • T/t: 时间计算,要求用于计算的值是"HH:MM[:SS]"的形式,当使用 T 时,输出结果是 "HH:MM:SS" 形式;使用 "t" 时,结果显示为一个数值,默认情况下单位是小时,可以通过变量 org-table-duration-custome-format 来设置
  • E: 不使用时,所有空白单元格都会被跳过,不会包含在计算过程中;当使用时,如果还使用了 N ,则用 "0" 填充;否则,在普通公式中,用 "nan" 填充,在 emacs lisp 公式中,用空字符串填充
  • N: 使用时,将所有域的值视为数字,对于非数值型,用 0 替代
  • L: 只用于 emacs lisp 公式,后续

如果需要对表格公式的求值进行调试,可以通过快捷键"C-c {"来开启调试模式(或者关闭它)。

表格绘图

使用 Org mode 文档中的表格数据进行绘图有两种方式,一种是使用 Org mode 提供的 "org-plot/gnuplot" 命令直接绘制图像,另外一种是通过在 source block 中读取表格数据来绘图。前者胜在方便快捷,但需要对 gnuplot 有一定的了解;后者胜在灵活,可以选用自己擅长的可视化方法,而且可以绘制复杂的图形。

org-plot/gnuplot

第一种方法依赖 gnuplot 这个外部绘图工具,以及 gnuplot-mode 这个 Emacs 插件。

在依赖满足的情况下,只需要在表格上方添加 "#+PLOT:", 然后在后面填写要传递给 gnuplot 的参数即可:

#+PLOT: title:"Citas" ind:1 deps:(3) type:2d with:histograms set:"yrange [0:]" file:"./plot.png"
| Sede      | Max cites | H-index |
|-----------+-----------+---------|
| Chile     |    257.72 |   21.39 |
| Leeds     |    165.77 |   19.68 |
| Sao Paolo |     71.00 |   11.50 |
| Stockholm |    134.19 |   14.33 |
| Morelia   |    257.56 |   17.67 |

得到的结果如下图所示:

org-plot-example.png

使用这种 Org mode 自带的绘图方式,除了简便以外,还有一个好处就是表格的 header 能被正确地识别做列名,并在图中用来作为各列数据的 label。

以下是可在 "#+PLOT:" 后面设置的绘图参数

  • title: 设置图像的标题
  • ind: 用于绘制 x 轴的表中的列
  • deps: 除 x 轴以外的其他数据在表中的列,若有多列,用括号括起,如 "deps:(2, 3)"
  • type: 2d, 3d, or grid
  • with: 设置绘制类型,如 lines, points, boxes, impulses, histograms
  • file: 如果需要将绘制的图像保存为文件,则使用该属性
  • labels: 给定 deps 的标签,默认为表格 header

详见: Plotting tables in Org-Mode using org-plot

With source block

Org mode 有一个非常强大的功能就是可以插入各种语言的 source block,并且能去执行 source block 里的代码,接着将结果插入到当前的 Org mode 文档中来。

下图展示了在 Org mode 中插入 C++ 的 source block 并执行得到结果的过程:

org-src-block-evaluate.gif

同时,Org mode 中的表格数据是可以作为变量传递到 source block 中的,如下图所示:

org-src-block-read-tbl.gif

如上图所示,要将表格数据传递给 source block ,需要两个步骤

  1. 用 "#+NAME" 将表格命名为 "citas-data"
  2. 在 source block 的选项中,用 ":var tbl_data=citas-data" 将表格数据赋值给变量 "tbl_data"

对于下面这个表格,我可以可以用这个方法将数据传递给 source block ,然后用 matplotlib 来绘制图像。

#+NAME: citas-data
| Sede      | Max cites | H-index |
|-----------+-----------+---------|
| Chile     |    257.72 |   21.39 |
| Leeds     |    165.77 |   19.68 |
| Sao Paolo |     71.00 |   11.50 |
| Stockholm |    134.19 |   14.33 |
| Morelia   |    257.56 |   17.67 |

相应的 source block 为

#+BEGIN_SRC python :results file :var tbl_data=citas-data filename="./org-plot-example2.png"
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

plt.style.use('ggplot')


bar_names = [row[0] for row in tbl_data]
h_index = [row[2] for row in tbl_data]
ind = np.arange(len(tbl_data))
width = 0.5

plt.bar(ind, h_index, width)
plt.title('Citas')
plt.xlabel('Sede')
plt.ylabel('H-index')
plt.xticks(ind + width/2., bar_names)

plt.savefig(filename)
return(filename)
#+END_SRC

执行后得到的结果为:

org-plot-example2.png

总结

同 Org mode 中的其他功能一样,表格能很好地和 Org mode 的其他功能一起工作,利用 Org mode ,能够很容易地进行可重现研究(Reproducible Research),数据、处理过程、结果展示,都可以在 Org mode 中一起呵成,而且比起 IPython Notebook,Org mode 拥有丰富得多的功能,表格就是其中一例。

以数据分析为例,工作流程可以是这样的:

  1. 将收集到的数据整理成 CSV 格式(比如从 excel 中导出)
  2. 在 Org mode 中将 CSV 格式的数据导入并创建一个表格,为其命名
  3. 新建一个 Python 的 source block, 对数据进行清洗、去重,并将结果输出,在 Org mode 中产生一个新的表格,同时将新的表格导出为 CSV 备份
  4. 对清洗后的数据进行分析,新建一个 Python 的 source block,使用 matplotlib 将分析结果绘制成图像并在 Org mode 文档中展示
  5. 对分析结果进行归纳总结
  6. 根据需要,使用 Org mode 自带的功能将文档导出为 HTML 或 PDF 格式,以供他人阅读
  7. (可选)将该 Org mode 文档分享给其他 Org mode 用户

Org mode 真的是太棒啦!

强大的 Org mode(2): 任务管理

2015年7月15日 08:00

本文是《强大的 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

基本的任务管理

在 Org mode 中,可以在 headline 的星号与内容之间插入特定的表示进度的关键词,来将一个 headline 标记为一个任务(Plan),默认情况下,支持两个关键词,分别是 "TODO" 和 "DONE"。当一个 headline 作为一个任务时,一方面标示其状态的关键词会被高亮(如下图所示),另一方面 Org mode 提供了一组完善的功能来对这些任务进行处理。

org-task-status.png

对任务管理而言,最基本的功能,就是快速地更改任务的状态,在这里,用户当然不用手动去修改表示任务状态的关键词,Org mode 已经提供了这样的一组快捷键给用户:

快捷键 功能 备注
C-c C-t 按照 无状态->TODO->DONE->无状态 的顺序更改任务状态 org-todo
Shift-<right> 同上  
Shift-<left> 按照与 Shift-<right> 相反的顺序更改任务状态  

下图是一个示例:

org-task-status-change.gif

除此以外,Org mode 也提供了快速创建任务的操作

快捷键 功能 备注
C-S-return 在当前任务的内容后面建立一个同级任务,标记为TODO 无任务时创建一级任务,标记为TODO
M-S-return 在当前任务后建立一个同级任务,标记为TODO 同上

上表中的“任务”指的就是带有相关关键词的 headline,由于 headline 是可以分级嵌套的,所以这里的任务也可以分级嵌套,这也是 Org mode 中任务结构的基础。有关子任务的话题将在后面补充。

自定义状态序列

添加新的状态

上一节提到,默认情况下,Org mode 接受的任务状态关键词只有 "TODO" 和 "DONE" 两个,分别表示 "待办" 与 "完成"。这样的分类对于一些不可再分的小任务,当然是足够用的,但有时候需要设置更为丰富的任务状态,如添加一个表示 "正在进行" 状态的关键词 "DOING",在不进行设置的情况下,这个关键词是无法高亮的:

org-task-add-doing.png

这个词将会被视作任务名称的一部分,而不是表示任务状态的标识。用本文之前提到的快捷键更改任务的状态,会看到下面这样的变化。

org-task-status-doing-ignored.gif

要达到预期的目的,有两种方法,一种方法是全局的,另一种则只在当前文件生效。

Org mode 中有一个变量 org-todo-keywords 存储着作用于全局的状态序列,它的默认值是:

'((sequence "TODO" "DONE"))

这就是默认状态只有两个的原因,修改这个变量的值也就能达到全局修改状态序列的目的了。

需要注意的是,状态序列在定义时的顺序是从左至右有序的,最后一个关键词会被认为是 表示任务终结 的关键词。所以如果要添加 "DOING" 这个状态,应该像下面这样进行设置:

(setq org-todo-keywords '((sequence "TODO" "DOING" "DONE")))

默认情况下表示非终结状态的关键词与表示终结的关键词的高亮颜色是不一样的,而且在更改为终结状态时,会在任务下添加一条下面这样的标记。

CLOSED: [2015-07-15 三 23:20]

如果将 "DOING" 放置到序列的末端,那么状态变化时的现象就会和预期的不一致:

org-task-wrong-status.png

如果只想在某个文件中为其设置独有的关键词序列,那么可以在org文件的头部用"#+SEQ_TODO"来进行设置:

#+SEQ_TODO: TODO DOING DONE

为状态设置不同外观

之前提到,Org mode 在高亮时只区分非终结状态与终结状态,但如果各个状态都能以不同的颜色显示,那肯定能让任务的状态更加一目了然。通过修改 org-todo-keyword-faces 这个变量可以达到这个目的。例如我们希望 "TODO" 以红色显示,"DOING" 以黄色显示,"DONE" 用绿色显示,就可以这样设置:

(setq org-todo-keyword-faces '(("TODO" . "red")
                               ("DOING" . "yellow")
                               ("DONE" . "green")))

以下是这样设置后的效果图:

org-todo-keywords-face.png

多个终结状态及快速选择

前面所以说 "终结状态" 而不说 "完成状态",是因为 "终结状态" 可能有不止一种,比如在表示正常完成的状态外,还可以有表示异常中止的状态。当有多个表示终结的状态时,相应的关键词要处于关键词序列的尾部,并且用"|"和非终结状态分隔开来,也就是这样:

(setq org-todo-keywords '((sequence "TODO" "DOING" "|" "DONE" "ABORT")))

若在文件中进行设置,则是:

#+SEQ_TODO: TODO DOING | DONE ABORT

这样的话有一个问题,那就是,一般来说,对于一个任务来说,一般只会用到多个终结状态中的一个,但假如我要将一个原先为"TODO"状态的任务标记为"ABORT",我就要使用快捷键"C-c C-t"或"S-left"/"S-right",依次经过"DOING"、"DONE",最后才标记为"ABORT"。这样的行为和理想的体验是不符合的。

好在 Org mode 也提供了解决这一场景的方法,那就是,可以为每一个状态设立一个快速选择键,在使用快捷键"C-c C-t"时,会等待输入这个快速选择键来迅速指定为特定状态。使用这个功能所需要的设置也很简单。

第一个是将变量"org-use-fast-todo-selection"的值设置为真(t)——一般来说,这个变量的值默认就是真(t):

(setq org-use-fast-todo-selection t)

然后在定义关键词序列时,在每个关键词后跟随括号并在其中指定快速选择键,如:

(setq org-todo-keywords '((sequence "TODO(t)" "DOING(i)" "|" "DONE(d)" "ABORT(a)")))

或在文件头部设置:

#+SEQ_TODO: TODO(t) DOING(i) | DONE(d) ABORT(a)

这样使用快捷键"C-c C-t"时,就能够方便地切换任务的状态了,如下图所示:

org-task-fast-select.gif

进入与离开时的额外操作

除了上述内容以外,Org mode 还允许定义进入状态和离开状态时的额外动作,可用的动作包含两个:

  • 添加笔记和状态变更信息(包括时间信息),用"@"表示
  • 只添加状态变更信息,用"!"表示

这个通过定义带快速选择键的关键词时,在快速选择键后用"X/Y"来表示,X表示进入该状态时的动作,Y表示离开该状态时的动作。对于一个状态(以"DONE"为例),以下形式都是合法的:

DONE(d@)       ; 进入时添加笔记
DONE(d/!)      ; 离开时添加变更信息
DONE(d@/!)     ; 进入时添加笔记,离开时添加变更信息

而这个是不合法的:

DONE(d@/)

需要注意的是,当由状态 A 到达状态 B 时,为状态 A 设置的离开动作 YA 只在状态 B 未设置进入动作 XB 时生效;如果目标状态设置了进入动作,那么出发状态的离开动作不被执行,而是执行目标状态的进入动作。下图为演示。

org-task-changing-action.gif

基于列表的任务

除了基于 headline 的任务管理外,Org mode 还提供基于列表的任务管理,即将每个列表项作为任务,方法是在列表标记与列表项内容之间添加一个 "[ ]" 标记(注意中间包含一个字符的预留位置),这个标记在 Org mode 中被称为 checkbox 。这种任务只有三种状态(待办、进行中和完成),分别用 "[ ]", "[-]" 和 "[X]" 表示,如下图所示:

org-checkbox-task.png

若要将用 checkbox 标记的任务标记为完成,将光标移动到对应的行,然后使用快捷键 "C-c C-c" 即可。对于包含子任务的任务,如果其子任务未全部完成,用此快捷键更改其子任务状态时,该任务的状态会自动设置为 "进行中([-])",表示子任务未全部完成;当用快捷键将所有子任务标记为完成时,它会自动更新为完成状态。

用"TODO"等关键词标记为headline为任务时,使用的快捷键同样适用于checkbox,不过略有不同:

快捷键 功能 备注
C-S-return 在当前列表项的内容后面建立一个同级列表项,标记为 "[ ]" 无列表项时不创建
M-S-return 在当前列表项后建立一个同级列表项,标记为T"[ ]"  

checkbox 任务相对来说更轻量、简洁一些,自然也会有人群更倾向于使用这种方式进行任务管理。不过就我个人而言,基于 headline 的任务管理可以设置 deadline,可以设置为周期循环任务,还可以计时统计,这些功能是更有吸引力而且有必要的。

子任务、任务进度与任务依赖

在之前本文提到过 "子任务" 这个概念,这个在 Org mode 里的表现形式是很简单的:

  1. 一个基于 headline 的任务,如果其下的更低等级的 headline 也被作为一个任务,那么这个更低等级的任务就是子任务
  2. 一个用列表项表示的任务同理

对包含子任务的任务,可以在该任务中加入 "[\/]" 或者 "[\%]" 来实时展现该任务的完成进度跟踪,在用快捷键来改变子任务的状态时,父任务的完成进度会被自动更新,当然,也可以执行函数 org-update-statistics-cookies 来手动更新,默认情况下,这个函数被绑定到快捷键 "C-c #" 上。

下图是对 checkbox 任务进行更新时进度的自动更新情况,基于 headline 的任务进度更新类似。

org-task-update-cookie.gif

对于包含子任务的复杂任务,除了进度跟踪外,Org mode本身还提供两种简单的任务依赖处理:

  1. 当任务还有子任务未完成时,阻止任务从未完成状态到完成状态的改变
  2. 对基于 headline 的任务而言,若其上一级任务设置了 ":ORDERED:" 属性,则在其前面的同级任务完成前,无法被设置为完成状态

不过以上两种行为默认是被关闭的,如果要开启这些功能,要对变量 org-enforce-todo-dependencies 进行设置:

(setq org-enforce-todo-dependencies t)

效果如图:

org-task-dependencies.gif

当然,还可以通过org-depend.el实现更复杂的依赖处理,这里就不做深入展开了。

强大的 Org mode(1): 简单介绍与基本使用

2015年7月12日 08:00

本文是《强大的 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

简介

Org mode 是 Emacs 的一个插件,能为 Emacs 用户提供一个强大的纯文本编辑环境,下面是 Org mode 官网上对自己的介绍:

Org mode is for keeping notes, maintaining TODO lists, planning projects, and authoring documents with a fast and effective plain-text system.

Org mode 实际上是一种轻量级标记语言,与 RSTMarkdown 类似,不过要比这两者拥有更为强大的功能和特性,是众多 Emacs 用户重度依赖的一个插件,对我个人来说也是构成日常工作、生活必不可少的重要工具。正如它的自我介绍所言, Org mode 与 RST 、 Markdown 相比,除了作为编辑环境以外,还可以进行任务管理、项目规划、笔记收集整理等各种操作 —— 事实上由于 Org mode 作为 Emacs 的插件,构建在 emacs lisp 语言之上,也使得它具备了无与伦比的可扩展性,但同时由于有统一的开发团队进行维护,而使得其语法规则没有因为强大的可扩展性而导致不同方言的泛滥,而 Markdown 就没有避免这个问题。

需要说明的是,作为一个标记语言, Org mode 的基本语法规则其实是很简单的,但它同时还提供了大量的“功能”使得其变得异常强大,但这些功能都是构建在基本的语法规则之上的。对于初学者来说,Org mode 能够很快地上手,而在上手后又还有非常丰富的内容可待探索。

以下是Org mode的几大特性:

  • 基于大纲的编辑(outline-based editing)
  • 灵活强大的任务管理(planning)
  • 任务计时及统计(clocking)
  • 日程管理(agendas)
  • 快速捕获(capture)
  • 功能丰富的表格操作(tables)
  • 导出到多种外部格式(exporting)
  • 文学编程(working with source code)
  • 移动端支持(with your mobile phone)

下图是Org mode官网上的一张示例图,对上述一些特性(outline-based editing, planning, agendas)进行了展示。

org-main.jpg

安装与基础设置

自 Emacs 22 后,Emacs 都自带了可用的 Org mode,在 Emacs 中,Org mode 作为一个 major mode,在用 Emacs 打开(或新建)后缀为 "org" 的文件时即会启用,如果您的 Emacs 在打开 org 文件时没有启用 org-mode 这个 major mode,可以在配置文件中加入:

(add-to-list 'auto-mode-alist  '("\\.org\\'" . org-mode))

另外 Org mode 在默认情况下不开启自动折行,这将导致一行文字的长度超出屏幕范围时,行会继续往右延伸而导致部分内容不可见(因在屏幕范围外而无法看见),要开启自动折行,应在配置中进行设置:

(setq truncate-lines nil)

除此以外,基本上就不用进行其他设置了(至少基本功能的使用是不需要的)。

需要注意的是,Emacs 自带的 Org mode 一般版本都偏旧,存在一些已知的 bug,建议替换为新版本,要达到该目的有两种办法:

  1. 若 Emacs 版本为 24 或更新,可以通过内置的包管理器 elpa 来安装新版的 Org mode,使用 package-install 命令并在要求输入包名时输入 org 。这种方法安装的 Org mode 不需要进行额外配置即可在 Emacs 启动时被加载。
  2. Org mode的官网 上下载最新的包,解压后在 Emacs 的配置文件中添加配置进行加载,比如将其解压在 ~/.emacs.d/site-lisp/org/ 目录下,那么配置语句可以是这样的:

    (add-to-list 'load-path "~/.emacs.d/site-lis/org/lisp")
    (add-to-list 'load-path "~/.emacs.d/site-lis/org/contrib/lisp")
    

    相比用 elpa 安装的方法,这种方法的一个优点是能使用一些其他开发者贡献的、暂未成为核心功能的增强功能,就是上面的第二条配置语句的目的。

基本使用

所谓大纲(outline)

在 Org mode 中,文档内容是通过 headline 来组织成一个树状结构的 —— 由于在 Org mode 中还存在 "title" 这种文章级的标题,为避免混淆,就不对 "headline" 进行翻译了,我们只要知道这几点就行:

  1. headline 是一节内容的标题(概要)
  2. headline 可以分级
  3. headline 可以有子 headline

我想凡是有 Markdown 、RST 乃至 Microsoft Office 使用经验的人,都能理解什么是 "headline"。

headline 在 Org mode 中会被高亮显示,且不同级别的 headline 会以不同的颜色显示。要创建 headline 也很简单,只要一行文字以若干个连续星号(*)顶格,并在星号结束后跟随至少一个空格,则该行会被视为一个 headline ,连续星号的数量被视为 headline 的层级,如图所示。

实际上 Org mode 提供了丰富的快捷键来操作 headline,不需要手动输入星号(*)来创建 headline。以下是这些快捷键的一个总结:

快捷键 功能 备注
C-<return> 在当前 headline 所属的内容后建立一个同级 headline 无 headline 时创建一个一级 headline
M-<return> 在当前 headline 后建立一个同级 headline 同上
M-<right> 降低当前 headline 的层级  
M-<left> 提高当前 headline 的层级  
M-<up> 将当前 headline 及其内容作为整体向上移动  
M-<down> 将当前 headline 及其内容作为整体向下移动  

通过 headline 来组织文档内容便于组织思想,这在很多其他编辑环境或者软件上都有体现,但与它们不同的是,Org能够方便地隐藏各级 headline 下的内容只显示 headline 的树状结构或者只显示第一级headline——这就是所谓 outline 了,如下图所示。只要通过 headline 组织好 Org 文档,概览文档、快速定位和编辑都能很方便地做到。

org-outline.png

通过快捷键 TAB 可以对某个 headline 及其内容的显示在三种状态(Folded, Children, Subtree)中切换;快捷键 S-TAB 则对整个 org 文件的内容显示在三种状态(Overview, Contents, Show all)中切换。Org 的文档称这种行为为"cycling",前者称为"subtree cycling",后者则是"global cycling"。

subtree cycling的状态

  • Folded: 对当前 headline,只显示 headline,其下的子节点及内容隐藏
  • Children: 对当前 headline,显示当前 headline 及其下更低一级的 headline
  • Subtree: 对当前 headline,只显示当前 headline 及其下更低级的 headline

下面的动态图像展示了subtree cycling 的状态变化(Folded -> Children -> Subtre)。

subtree-cycling.gif

global cycling 的状态变化与 subtree cycling 类似,不过针对的是整个文档,下面图像展示了 global cycling 的状态变化(Overview -> Contents -> Show all)

global-cycling.gif

Org mode 的几大特性中,任务管理、任务计时、项目管理都是建立在 outline 结构上的,捕获虽然可以不基于这个结构,但一般来说都会使用。

基本语法

除了 headline 外,Org mode 还支持列表、文字修饰(粗体、斜体、下划线等)、代码块、引用等常见的功能。

  • 列表

    + 无序列表
      * 用"+","-", "*"开头,后跟随用空格分隔开的列表项名称、内容
      * "*"在行首顶格的话则是headline,这个要注意
      * 如果想结束一个列表,那么在其后跟随两个空行
    
    + 有序列表
      1. 用"1.","1)"开头,后跟随用空格分隔开的列表项名称、内容
      2. 其余同无序列表
    

    列表和 headline 一样,也是可以分级、嵌套的,列表相关的快捷键也与 headline 相关的快捷键部分重叠(Org mode 会根据实际情况来进行合理的操作,所以不用担心混淆问题):

    快捷键 功能 备注
    C-<return> 在当前列表项的内容后建立一个同级列表项 光标在列表项同一行时有效
    M-<return> 在当前列表项后建立一个同级列表项 同上
    M-<right> 降低当前列表项的层级 同上
    M-<left> 提高当前列表项的层级 同上
    M-<up> 将当前列表项及其内容作为整体向上移动 同上
    M-<down> 将当前列表项及其内容作为整体向下移动 同上
  • 粗体

    粗体 用两个星号包裹,星号与粗体前后的其他字应该各有至少一个空格或英文标点分隔开来
    bold,word,用英文逗号分隔开,bold显示出粗体效果
    *bold*word则不显示粗体效果
    
  • 斜体

    hello 用""包裹,规则同粗体
    斜体 好像对中文不管用?导出成HTML时看到还是生效了,可能是字体关系吧
    
  • 下划线

    下划线 用"_"在前后包裹,规则同粗体
    _下划线_紧跟其他字,则不生效
    
  • 删除线

    删除线 用"+"在前后包裹,规则同粗体
    
  • 引用块

    #+BEGIN_QUOTE
    引用内容
    #+END_QUOTE
    

    在org文件中输入"<q"然后按 TAB 键会自动插入一个引用块,插入后在其中进行编辑即可。

  • 示例块

    #+Begin_EXAMPLE
    示例
    #+END_EXAMPLE
    

    在org文件中输入 "<e" 然后按 TAB 键来插入一个示例块,可以移动到其中并使用快捷键"C-'(单引号)"来在新窗格中编辑它,编辑好后用同样的快捷键来保存。

  • 代码块

    #+BEGIN_SRC C++
    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char *argv)
    {
        cout << "代码块" << endl;
    
        return 0;
    }
    #+END_SRC
    

    代码块稍微复杂一点,插入的话是通过输入"<s"然后按 TAB 键来达到,但插入后还要在"#+BEGIN_SRC"后指定代码的语言类型(如上例所示)。

    Org mode可以根据代码所属的语言来对其进行高亮,不过自Emacs 24.1后,默认设置下不对代码块进行高亮,需要进行设置

    (setq org-src-fontify-natively t)
    

    在编辑代码块的时候也会开启对应语言的 major mode,若对应的语言配置得当,在编辑 Org mode 中的代码块时,自动缩进、自动补全等功能都可以享用。编辑与保存的快捷键同示例块(exmple block)。

    source-edit.gif

    此外在 Org mode 中还可以对代码进行求值,产生了很多丰富的应用方式,这个留待后续。

  • 图片

    在 Org mode 中,可以插入本地图片,并在 Org mode 中进行显示。要做到这个,直接在 org 文件中写入本地文件的路径,然后开启 iimage-mode 即可(M-x iimage-mode)。

    为进行区分,通常会在图片地址前加上"file"前缀,如

    file:/assets/img/source-edit.gif
    
  • 链接

    链接的语法规则是这样的:

    [[<link url>][<text>]]
    

    比如

    [[http://linusp.github.io/][Linusp's Blog]]
    

    会显示为 Linusp's Blog

Emacs中的窗口操作

2015年4月19日 08:00

窗口

这里的窗口并不是指桌面环境中的窗口,而是指 Emacs 中的显示单元。在 Emacs 中,显示一个缓冲区内容的区域,就被称为 "窗口"。

一个窗口中只能显示一个缓冲区,而一个缓冲区是可以显示在多个窗口中的。出于一些需要,我们可能在使用 Emacs 的时候将整个 Emacs 显示区域分割成多个窗口,本文就将对这种情形下对窗口的常用操作做简要介绍。

窗口的新增

新增窗口是通过对当前窗口的分割来产生的,分割有两种方式:

  • C-x 2: 将当前窗口分割为上下两个,如下图所示

    emacs-window-split-hor.gif

  • C-x 3: 将当前窗口分割为左右两个,如下图所示

    emacs-window-split-ver.gif

分割后的子窗口都想显示被分割的窗口当时显示的缓冲区,如果想要在分割后切换到另外一个缓冲区,以下我编写的方法可以作为参考:

;; 将当前窗口分割为上下两个,并切换到另外一个 buffer
(defun split-window-new-buffer ()
  (interactive)
  (split-window-below)
  (call-interactively 'switch-to-buffer))

窗口的删除

窗口的删除有三个可用的快捷键,分别是:

  • C-x 0: 删除当前窗口
  • C-x 1: 删除当前窗口外的其他窗口
  • C-x 4 0: 删除当前窗口,并关闭其中显示的缓冲区

窗口的切换

默认的方法

Emacs 中提供了 C-x o(字母) 这个快捷键来在窗口间循环移动,方向大致是顺时针方向。分割的窗口数稍微一多的话,这么一个快捷键肯定是不够的。

事实上 C-x o 是调用了 other-window 这个内建方法,通过 C-h f 可以知道这个方法的原型是:

(other-window COUNT &optional ALL-FRAMES)

它的参数里我们要关心的是 COUNT 这个参数,这个参数可以取三种值:

  • 为正整数: 顺时针前进指定数目的窗口
  • 等于0: 原地不动
  • 为负整数: 逆时针前进指定数目(COUNT的绝对值)的窗口

所以我们可以很自然地写出一个 "切换到上一个窗口" 的方法来:

(defun prev-window ()
  (interactive)
  (other-window -1))

然后可以按自己的需要绑定到指定的快捷键上,以 C-x p 为例:

(global-set-key (kbd "C-x p") 'prev-window)

windmove package

Emacs 中其实还内置了更为简易的切换方法,可以向当前窗口指定方向的邻近窗口切换,不过并未给这些方法提供默认的快捷键绑定。

共有四个方法,它们在 windmove 这个包中被定义和实现:

  • windmove-up
  • windmove-down
  • windmove-right
  • windmove-left

功能的话看它们的名称就看得出来,我想就不用多解释了。

我们可以自己来进行绑定,或者也可以使用默认的设置:

(windmove-default-keybindings)

在配置文件中添加上面这句后,我们将可以使用 Shift+方向键 的方式来进行窗口切换。为求方便一般还会在其后跟上以下这句:

(setq windmove-wrap-around t)

这一条语句的作用是让 windmove 在边缘的窗口也能正常运作。举个例子,当前窗口已经是最左端的窗口了,如果使用 Shift+left ,将仍会停留在当前窗口——因为已经到边缘了,左边没有窗口可供选择。但在添加了上面这句后,Shift+left 将会跳到最右边的窗口中。垂直方向上的窗口切换同理。

窗口布局的保存与恢复

Emacs 中已经内置了窗口布局的保存与恢复功能,通过在配置文件中添加下列语句就可以启用布局保存功能:

(desktop-save-mode 1)

初次启用的时候会询问是否保存 "desktop" ,这个时候需要选择保存位置,布局一般会保存为名为 .emacs.desktop 的文件。

desktop-save-mode 不仅会保存窗口布局,还会保存每个窗口中打开了什么缓冲区,每个缓冲区中的光标位于哪一行这些非常游泳的信息,所以还是非常方便的。

不过虽然保存了 desktop 文件,Emacs 启动的时候却不会去主动加载它,一种办法是在配置文件里定义一个变量保存 desktop 文件所在的目录,然后在启动的时候就加载它:

(setq desktop-dir "~/Dropbox/doc/")
(desktop-read desktop-dir)

不过上面这样的设置是不够完美的,假如我们不带任何参数启动 Emacs ,当然是没什么问题,但如果我们是用 Emacs 打开某个文件,这样做明显是不合理的。

在Org-mode中显示特殊字符

2014年2月22日 08:00

在Org-mode中编写数学公式

在Org-mode中可以编写符合Latex语法的数学符号及公式,并且在发布成网页时以易读的形式展示。

比如下面这段语句:

$$e^{i\pi} + 1 = 0$$

会显示成: \[e^{i\pi} + 1 = 0\]

要启用这个功能,需要在发布成网页时在模板头部中包含:

<script type="text/javascript" src="http://orgmode.org/mathjax/MathJax.js"></script>
<script type="text/javascript">
  <!--/*--><![CDATA[/*><!--*/
    MathJax.Hub.Config({
        // Only one of the two following lines, depending on user settings
        // First allows browser-native MathML display, second forces HTML/CSS
        //  config: ["MMLorHTML.js"], jax: ["input/TeX"],
            jax: ["input/TeX", "output/HTML-CSS"],
        extensions: ["tex2jax.js","TeX/AMSmath.js","TeX/AMSsymbols.js",
                     "TeX/noUndefined.js"],
        tex2jax: {
            inlineMath: [ ["\\(","\\)"] ],
            displayMath: [ ['$$','$$'], ["\\[","\\]"], ["\\begin{displaymath}","\\end{displaymath}"] ],
            skipTags: ["script","noscript","style","textarea","pre","code"],
            ignoreClass: "tex2jax_ignore",
            processEscapes: false,
            processEnvironments: true,
            preview: "TeX"
        },
        showProcessingMessages: true,
        displayAlign: "center",
        displayIndent: "2em",

        "HTML-CSS": {
             scale: 100,
             availableFonts: ["STIX","TeX"],
             preferredFont: "TeX",
             webFont: "TeX",
             imageFont: "TeX",
             showMathMenu: true,
        },
        MMLorHTML: {
             prefer: {
                 MSIE:    "MML",
                 Firefox: "MML",
                 Opera:   "HTML",
                 other:   "HTML"
             }
        }
    });
/*]]>*///-->
</script>

在将单个org-mode文档导出成网页时,模板中时默认有此内容的,可以不用进行特别的设置。不过在将一个目录作为项目发布成网页时,出于简洁的目的,可能会有如下设置:

(setq org-publish-project-alist
      '(
        ("blog-org"
         ...
         :html-head-include-scripts nil
         ...)
        ...))

这条语句会使项目在发布时去除默认模板中包含的一些js片段,这是需要注意的地方。如果需要在项目发布中也启用数学符号/公式显示的功能,最好将这个选项打开,或者自定义也可以(但应该包含上面所示的js片段)

所见即所得:在org-mode中即时显示特殊字符、数学公式

其实在org-mode文档中也能在编辑好特殊字符、数学符号及公式后即时地显示,实现真正的“所见即所得”。

临时启用这个特性,只要在编辑org文档时执行:

C-c C-x \

这个快捷键会调用命令:

org-toggle-pretty-entities

效果如下:

org-pretty-entities.gif

不过效果并不是非常好,和Texmacs还有区别。对于单个的特殊字符如希腊字母,效果是可以的,但对于一些复杂的数学公式,比如

$$J(\theta) = \frac{1}{2m}\sum_{i=1}^{m}(\theta^{T}X_{i} - Y_{i})^2$$

理想的显示效果应该是: \[J(\theta) = \frac{1}{2m}\sum_{i=1}^{m}(\theta^{T}X_{i} - Y_{i})^2\]

但它的实际显示效果却是:

actually-pretty-entities.png

可以看出org-mode的这个功能中对特殊字符的解析和Latex的语法并不一致,至于是否存在解决办法,这个就有待以后讨论吧。

使用ox-freemind将org-mode文档导出成思维导图

2014年1月6日 08:00

思维导图

思维导图是一个很好地整理知识、表达思维的工具。因为使用Linux,我更经常使用Freemind这么一个开源的思维导图工具。

Free_Mind.png

Freemind本身也是一款很优秀的软件,它能够将思维导图导出成html、flah、Java Applet、Open Office文档以及PNG和JPEG两种格式的图片,并且对思维导图的绘制提供的快捷键。

这里 是Freemind的官网。

我曾经用Freemind绘制过一张Emacs的思维导图,放到了百度的emacs贴吧里,受到了一些Emacs新手的欢迎。因为那张图存在一定的错误,于是后来我决定重新画一张,而且决心要将更全面的内容表达出来。这是个非常累人的活,我断断续续用了一天时间才完成。

老实说Freemind已经很方便了,在熟悉了相关快捷键后,能够很高效地进行思维导图的绘制。

不过我希望能在更熟悉的环境中来做这件事情,我希望不用再去学习另外一套操作方式,而是能在org-mode中来绘制思维导图。

我的Org版本是8.2.3c,这个版本的org-mode中提供了ox-freemind.el来满足我这个要求。

使用ox-freemind.el

所有Org的核心模块都在 org/lisp/ 目录下,但这个插件并不是Org的核心模块,它被放置在 org/contrib/lisp 目录下,如果您没有在Emacs配置文件中将这个路径加入加载路径,那么先做好这件事,然后在配置文件中添加:

(require 'ox-freemind)

然后就可以使用 org-freemind-export-to-freemind 来将Org文档导出成Freemind文档了。

如,我新建了一个文件 mind.org ,内容如下

#+TITLE: Org-mode
** 写文档
** 发布成html
** org-bable
** 表格

执行 org-freemind-export-to-freemind 并用Freemind打开导出成图像后,得到的结果是:

mind.png

生成的思维导图样式是在ox-freemind.el中的变量 org-freemind-styles 中定义的。

ox-freemind.el中的bug

如果您使用和我一样版本的Org-mode,那么很可能会遇到和我一样的问题。在执行 org-freemind-styles 后出错,出错信息为:

org-freemind-export-to-freemind: Symbol's function definition is void: \,

最后我在Org-mode的邮件列表中找到了解决方法。原链接在此

出错的原因是 org-freemind-export-to-freemind 函数的定义中有一处错误。其原始内容为:

1: (defun org-freemind-export-to-freemind
2:   (&optional async subtreep visible-only body-only ext-plist)
3:     (interactive)
4:   (let* ((extension (concat ".mm" ))
5:          (file (org-export-output-file-name extension subtreep))
6:          (org-export-coding-system 'utf-8))
7:     (org-export-to-file 'freemind ,file
8:     async subtreep visible-only body-only ext-plist)))

将倒数第二行中的

org-export-to-file 'freemind ,file

修改为

org-export-to-file 'freemind file

嗯,没错,作者多写了一个逗号。

修改键位+使用smex,告别Emacs小指综合症

2013年12月31日 08:00

Emacs小指综合症

所谓“Emacs小指综合症”,是指由于长期使用Emacs导致左手小指疼痛的问题——嗯,没错,这是我下的定义——英文说法是 Emacs Pinky Problem 。这一问题的根源是因为Emacs的快捷键频繁使用键盘的 ctrl 键,而由于现在被普遍应用的 QWERTY 键盘布局上的 ctrl 键都只能用小指来按下(不过通常Emacser只使用左侧的 ctrl 键),且在按下 ctrl 时小指的负荷很大。

这个问题是Emacs被诟病的几个主要问题之一,不过严格来说,这并不是Emacs的错,因为Emacs被设计时考虑的键盘布局和现在是不一样的,那个时候的 esc 键和 ctrl 键都是在比较舒服的位置的,所以这是个历史遗留问题。

常规解决办法

修改键位或键盘布局

这种办法的核心思想是通过一些措施把 ctrl 键映射到物理键盘上比较好按的键位上。常见的方法是将 ctrl 键和 caps lock 键交换,在Planet Emacsen 上还提到了另外一种办法:将 ctrl 键和回车键交换。

使用正确的姿势

在笔记本的键盘按左 ctrl 键尤其难受,不过在笔记本键盘上,可以通过用手掌根部按压左 ctrl 键来避免使左手小指受损。

不过我并不习惯这种姿势。

使用合理的键盘

使用设计更加合理的、符合人体工程学的键盘是解决这个问题的好办法。这个方法不仅能避免Emacs导致的问题,还能减轻其他因为长期使用电脑/键盘而出现的健康问题。

当然,为了健康而投入资金是必须的。

使用smex插件

smex是Emacs的一个插件,这里 是相关说明。

smex是一个"M-x"快捷键的增强工具,它能够使得在Emacs中调用各种命令更为方便,能更智能地对命令进行补全,还能根据使用者调用命令的频率来猜测用户可能会执行的命令。如下图所示:

2013-12-31-smex-use.png

不少Emacs中的命令名字都很长,所以如果不使用smex的话,一来很多命令根本记不住,二来就算记住了输入时也要花费许多的时间——这也是为什么Emacs有如此多的快捷键的原因之一吧。

smex的使用也很简单,下载smex并放置到Emacs的加载目录中后,在配置文件中添加这么几条语句:

(load "~/.emacs.d/site-lisp/smex.el")
(require 'smex)
(smex-initialize)
(global-set-key (kbd "M-x") 'smex)
(global-set-key (kbd "M-X") 'smex-major-mode-commands)
(global-set-key (kbd "C-c C-c M-x") 'execute-extended-command)

我的办法呢,就是综合键位修改和使用smex两个方案。将 ctrlcaps lock 交换,然后绑定一些(少量)常用的命令到快捷键上,其他大部分的命令调用则使用smex。

org-mode导出项目时发布所有文件而不只是被修改的文件

2013年12月15日 08:00

问题

使用org-mode,可以将一个目录下的所有org文件作为一个完整的项目进行导出,这个功能常常用于将写好的org文件以完整的网站结构导出成html文件。

在这个过程中,有时候我们修改了项目的设置,比如说在 org-publish-project-alist 中修改了 html-preamblehtml-postamble 两个设置——这两个参数定义了项目中所有org文件导出成html时的模板。但如果修改后执行 org-publish-project ,这个改变并不会被应用,因为org-mode导出项目时会检查项目中的文件,然后仅发布被修改或未被发布过的文件。这就是本文要解决的问题。

解决方法

上述所说的org的特性——导出项目时仅对修改过或未被发布过的文件,是由org内置的变量 org-publish-use-timestamps-flag 来决定的。

通过快捷键 C-h v 查看该变量的文档,可以看到它的描述是:

org-publish-use-timestamps-flag is a variable defined in 'ox-publish.el'. Its value is t

Documentation: Non-nil means use timestamp checking to publish only changed files. When nil, do no timestamp checking and always publish all files.

根据文档,将这个变量设置为 nil 就可以改变上述org在导出项目时的行为,但是注意 always 这个词。如果在配置中将这个变量的值设置为了 nil ,那么每次导出项目时,所有文件都会被重新发布!而我们需要的是在 "需要的时候" 重新发布所有文件,每次都重新发布会带来不必要的时间消耗,这不是我们想要的。

我的解决办法是将 org-publish-project 这个命令包装成 publish-project ,每当这个命令执行时,它会询问是否要重新发布所有文件,如果是则将 org-publish-use-timestamps-flag 这个变量置为 nil ,待发布完后又重新设回 t

下面是我的实现方法:

(defun publish-project (project no-cache)
   (interactive "sName of project: \nsNo-cache?[y/n] ")
      (if (or (string= no-cache "y")
          (string= no-cache "Y"))
          (setq org-publish-use-timestamps-flag nil))
   (org-publish-project project)
   (setq org-publish-use-timestamps-flag t))

我将这个函数绑定到了 C-x p 这个快捷键上。

Symbol t may not be buffer-local问题

2013年12月2日 08:00

真是快要疯掉了……本来只是想配置好python的开发环境的,然后动了整理配置文件的念头,整理的时候更新了一些插件,结果插件和旧版本org-mode是冲突的,安装上新版本org-mode吧,因为emacs总会加载/usr/share/emacs/24.2/lisp/以及其他几个相关目录下的一些文件,而这些文件和org-mode有关的一部分又是按照旧版本来的,新旧版本一直冲突。一怒之下把这些文件全删了自己编译安装,但是编译安装的依然报错……

当时真是要疯掉了……

算了,说正事。

后来我在ubuntu系统里把对应的目录复制过来用来进行恢复,总算才好了。但是,当我尝试进行publish的时候,就……

一直一直一直一直一直一直报这个错误:

Symbol t may not be buffer-local

*Message*里只有这一条信息,没有出错位置,没有任何其他信息。Google了八百遍也找不到相关的issue,当时真的是把这个Linux Mint系统格了重装的心都有了……

后来发现自己的搜索有点问题,我是直接把这条出错信息贴在搜索框里的,而这样Google是会把这句话拆分成多个关键字来进行搜索的,而不是用这整句话!尝试用双引号括起来后终于得到了一条稍微有点参考价值的结果: 2013-12-02-search-symbol-t.png

这条信息向我指示了可能的出错位置:

make-local-variable should accept a quote instead var. otherwise, it would introduce error as “Symbol nil may not be buffer-local” and fail to start ESS.

我的配置文件里有这么一条:

(defun my-lisp-style ()
  (highlight-parentheses-mode t)
  (hs-minor-mode t)
  (set (make-local-variable 'electric-pair-mode) nil))

而我漏写了 electric-pair-mode 前面那个单引号。

终于对上了,知道是哪里出了错,感觉真他妈好(偶尔爆个粗口)!

org-mode 8.x导出html时代码块不高亮问题

2013年12月2日 08:00

问题

在我将org-mode更换为8.x版本后,按照之前的经验,无论怎么设置,导出成html时都不能产生语法高亮效果.

以前的做法通常都是在配置文件里加上这么两句:

(require 'htmlize) ;htmlize.el
(setq org-src-fontify-natively t)

但是这次这两句完全不起作用……

解决方法

最后在org-mode的网站上找到了相关的信息,那就是在设置org的导出项目时,要在对应的alist中添加一个 :publishing-function 参数,并设置其值为非nil值,如下所示:

(setq org-publish-project-alist
    '(
      ("project-name"
       ......
   :htmlized-source t
       ......
       )
       ......))

吐槽

org-mode 8.x的变化太多了……不知道官方有没有对这些改动之处做一个详细说明……虽然说文档都发生了改变,但到发生问题时再来找真的很费劲啊……

org-remember从org-mode中移除

2013年12月2日 08:00

因为Crow 的提议,我和他还有另外一个同学准备做一个开源项目,而在项目中需要使用python,因此这两天就在配置Emacs的python环境,同时把原来的Emacs配置文件整理了一下,这期间发生了不少问题。

首先是为了使用最新的Emacs,在系统里添加了PPA下载了Damien Cassou维护的Emacs版本 ,而python.el 在该版本Emacs下会出问题。重新从源里安装了Emacs 24后这个问题得到了解决。但因为我对配置文件的大幅度调整,导致Emacs一直提示

Can't load library: org

找不到该问题源头所在的我,尝试着删除了Emacs24自带的org,用上了8.2.1的版本,不想问题就这样解决了——唔,我还是知其然不知其所以然。

但后来又报错,说无法加载org-remember,我在org目录下一查找,发现居然没有了 org-remember.el 这个文件,一时间很困惑。

在Google上以 org-rememberorg-mode8.x 为关键字,也只搜索到两条结果,不过这两条搜索结果告诉了我事实的真相。

2013-12-02-search-org.png

原来org-mode 8已经不支持org-remember.el了。

org-remember.el的功能是建立在remember.el之上的,而remember.el并不是org-mode的一部分,估计org-mode的开发人员是为了不依赖remember.el,而将org-remember.el从新版的org-mode中移除了,并以org-capture.el来替换它。

因此,若要使用org-mode 8,相关的配置需要进行一定的更改

我在配置文件中为 org-remember 设置了快捷键,因此要将那条语句中的 org-remember 修改为 org-capture

其次,org-remember模板和org-capture的模板格式不一样,需要将原先org-remember的模板进行修改,对应的变量名 org-remember-templates 也要改为 org-capture-templaes

org-capture的模板格式可以到这里 进行详细地了解。

丧心病狂:用Emacs阅读PDF文档

2013年11月6日 08:00

DocViewMode

Emacs有个插件叫做DocViewMode, 为Emacs提供了对PDF、PS等格式文件的支持,其原理是将这些文件转换成png格式的图片,并在Emacs中显示。

安装

从Emacs 23开始,默认就安装了DocViewMode,不过为了能够正常工作,还需要在系统中安装这两个东西:

  • xpdf
  • ghostscript

安装好后,像打开常规文件那样打开PDF文件就行了。

感受

怎么说呢……打开小的PDF文件还可以,比如说这个:

2013-11-06-emacs-pdf-view.png

操作也简单,+/- 进行缩放,n/p进行翻页。

然后我丧心病狂地按了C-x C-f后输入了:

~/Dropbox/book/lisp/ANSI_Common_Lisp.pdf

然后就悲剧了……

所以说某篇文章说的把Emacs设置成默认PDF阅读器这种事还是不要做的好。

❌
❌