至尊 Emacs,捕获所有

最近在琢磨知识管理的事情,起因是再一次发现自己的记忆力够烂。之前遇到的问题,再次遇到时只隐隐有一些印象,不免要再去{搜索引擎,聊天记录,浏览历史}里寻找一番,浪费了很多的时间,有时候还不一定找得到。如果能随手把有价值的知识记下来,之后再看就方便太多。

前阵子试过用 org-mode 记笔记,效果不太好,主要原因是太懒。以做一个网页笔记为例,需要:

打开 Emcas -> 打开 org-capture -> 切到浏览器复制内容 -> 切回 Emacs 粘贴 -> 切到浏览器复制链接 -> 切回 Emacs 粘贴,保存。

太过繁琐,并且打断了正在进行的工作流,坚持了一段时间就懒得继续了。

这几天想了一想对我来说理想的知识收集、管理方案的方案,这套方案应该:

  1. 一键保存选中的信息,并标记元信息(来源,时间)等等。使用简单,没有干扰。
  2. 支持多种场景:浏览器中的网页,PDF,工作群聊等等。
  3. 维护简单,长期可靠。
  4. 支持跨平台,包括我正在使用的操作系统:macOS、Linux、Android。
  5. 能够在多个平台间同步。
  6. 使用 org mode。我还可以再用 Emacs 500 年。

折腾了几天,踩了一些坑,不过基本上都实现了。

收集

收集的方案大家应该都能猜到:org-capture + org-protocol。装好包,系统再注册一下对应协议便可使用。

Emacs 的配置

Emacs 上需要配置 org-capture 和org-protocol。我用的 Spacemacs,原生的应该也差不多。

org-protocol 加一条配置启用即可。

(with-eval-after-load 'org
  (add-to-list 'org-modules 'org-protocol)
  )

因为 autoload 的原因,spacemacs 启动时不会默认加载 org 和org-protocol,这时候调用时把 URL 当作文件。我直接在 config 里写了 (require 'org-protocol), 简单粗暴。

我的 Capture 模板是

(setq org-capture-templates
      `(("c" "Captured" entry (file ,(concat my/sync-folder "capture.org"))
         "* %t %:description\nlink: %l \n\n%i\n" :prepend t :empty-lines-after 1)
        ("n" "Captured Now!" entry (file ,(concat my/sync-folder "capture.org"))
         "* %t %:description\nlink: %l \n\n%i\n" :prepend t :emptry-lines-after 1 :immediate-finish t)
        )
      )

两个模板的区别是后一个有 immediate-finish 参数,不提示直接保存。按需调用就好。

Mac

我参考了Org Capture From Anywhere on Your Mac这篇文章来配置。作者大哥把 handler 都写好了,抄过来就可以用。代码在 http://github.com/aaronbieber/org-protocol-handler

有一个问题,作者的脚本在传 url 给emacsclient 之前会 unquote 一次。经过测试,应该是不再需要了,多 unquote 一次反而会弄乱结果。 parse.py 里的 raw_url = six.moves.urllib.parse.unquote(url) 一行删掉即可。

Linux

org-protocol 的官方页面介绍了好几种注册的方式,然而我用的 awesome wm 并不在里面= =。放狗一搜,配置 Linux 要比 Mac 简单太多,加一个配置项即可:

linux - How can I register a custom protocol with xdg? - Super User

Chrome

保存书签:

javascript:window.open('org-protocol://capture?template=c'+ '&url='+encodeURIComponent(window.location.href)+ '&title='+encodeURIComponent(document.title)+ '&body='+encodeURIComponent(window.getSelection()));window.resizeTo(0,0); window.moveTo(0,window.screen.availHeight+10);

这时候就可以在网页上选中文字 Capture 了。有个小缺点:不知为何,打开的新的 org-protocol 页面不会自动关闭。

效果如下:

随时随地,随心所欲的 Capture

上面的方法只适用于浏览器。到处 Capture 的思路也很简单,取出当前选中的字符,拼出来 org-protocol 的链接访问。问题就成了,如何取当前选择字符呢?

Mac 上我用的是 Hammerspoon。Hammerspoon 自带 API,不过需要软件支持。试了一下,Chrome 和钉钉不支持,猜测使用 Electron 的应用都一样,但是 Preview、企业微信、Emacs 这些软件支持,已经足够我使用了。

Linux 竟然更麻烦一些,没找到可以直接用的接口。好在 Linux 会把选中的文字放在 Primary 的剪贴板里, xclip -o 输出即可。只是偶尔会捕获到之前选中的内容。

org-protocol 的urlencode 方式跟标准有一些不同:Emacs 使用 \n 代表换行,而不是 \r\n ;空格不使用 + ,而是 %20 。Hammerspoon 和Awesome 都用 Lua 作为脚本语言,我从url-encode.lua抄了一段,修改了一下:

function encodeURI(str)
  if (str) then
    str = string.gsub(str, "([^%w _ %- . ~])",
                      function (c) return string.format ("%%%02X", string.byte(c)) end)
    str = string.gsub (str, " ", "%%20")
  end
  return str
end

其他语言的脚本也类似。

我的配置如下:

Hammerspoon:

hs.hotkey.bind(
  hyper, "C",
  function ()
    local focusedElement = hs.uielement.focusedElement();

    if (focusedElement == nil or focusedElement:selectedText() == '' or focusedElement:selectedText() == nil) then
      hs.notify.new({title="Capture", informativeText="No selected text"}):send()
      return
    end

    local focusedWindow = window.focusedWindow()

    local url = string.format("org-protocol://capture?template=c&url=hammerspoon&title=%s&body=%s", escapeUri(focusedWindow:title()), escapeUri(focusedElement:selectedText()))

    hs.notify.new({title="Capture", informativeText=focusedWindow:title() .. "\n" .. focusedElement:selectedText()}):send()
    hs.execute(string.format("open '%s'", url))
end)

Awesome:

awful.key({ modkey, "Control"}, "c", function ()
    awful.spawn.easy_async("xclip -o", function (out)
                             local c = client.focus
                             if c and out ~= '' and out ~= nil then
                               local command = string.format("xdg-open 'org-protocol://capture?template=c&url=%s&title=%s&body=%s'", "awesomewm", encodeURI(c.name), encodeURI(out));
                               awful.spawn(command)
                               naughty.notify({ preset = naughty.config.presets.normal,
                                                title = "Captured!",
                                                text = string.format("Title: %s\n\nDescription: %s", c.name, out)})
                             end
    end)
end)

这样一键就可以捕获各个地方的信息了。

Android

手机和平板上我用 Orgzly。选中文字,分享给 Orgzly 可以快速创建一个条目。然而 Orgzly 没有 Capture 模板,创建的条目格式不好看,我单独开了一个文件存Android的笔记。

在 Termux 上装 Emacs也许可以搞定,还没有想到很好的调用termux里org-protocol 的方法。可能某些自动化工具能解决。

同步

同步对我来说是一个强需求。同一本书,取决于场景,我可能会在笔记本上,台式机,平板上看,需要有一个简单的方法将笔记汇总在一起。

最重要的问题是选择一个靠谱的同步盘。试用了 Dropbox,坚果云,OneDrive 后,我最终选择了 OneDrive。

Dropbox 本身的客户端和第三方接口支持都做的最好。只可惜在 Dropbox 现在的政策下,免费版只允许三个客户端同时登录,很是鸡肋。另外服务在国内被墙了,网络不稳定的情况下会很尴尬。(点名鹏博士)

坚果云是国内同步盘中做的比较好的,优点是客户端全面,也木有墙捣乱。缺点是 Linux 上的客户端很不稳定(我只成功启动过一次),而且第三方应用几乎都不支持坚果云的接口。

OneDrive 没有 Linux 客户端,不过经过试用,第三方客户端abraunegg/onedrive已经足够稳定。网页版的 OneDrive 在我这里经常无法访问,但是客户端传输是正常的。 Android OneDrive 的第三方客户端 OneSync,可以和在电脑上一样同步文件。配合 Orgzly 使用同步体验很好。

接下来…

还有几个问题不是很完美:

  • 目前只支持文字和链接,不支持捕获图片、文件等等。应该可以通过自定义org-protocol的handler配合org-download来解决。
  • Android的Capture体验比较差。有时间打算写一个小App,注册分享的接口。功能只需要读取Share的内容,格式化后往某个文件里Append(听起来so easy哈哈)。

尽管如此,现在已经可以覆盖我的大部分需求了,整体而言甚得我心。之前也用过一些商业化的笔记软件(为知,Evernote,有道云笔记等等),不爽的地方主要有:

  1. 内嵌的Markdown编辑器太弱。
  2. 产品的演化方向用户是没有发言权的,商业化目的的产品决策很少会符合用户利益。
  3. 数据掌控在企业手中,想迁移导出都很麻烦。

现在全靠Emacs + 同步工具就搞定啦。

有诗赞曰:

One Emacs to capture them all, One Emacs to ripgrep them,
One Emacs to organize them all and in the darkness bind them
In the Land of Internet where the Information lies.

My PRECIOUS.:crazy_face:

27赞

org-mode 做书签的话,可以参考一下我这个

1赞

哇 你这个比我的高级太多了

参考 https://github.com/sprig/org-capture-extension ,什么配置都全了呀。

4赞

挺强大,有点以前google note的感觉。不过现在很少大段复制做笔记了,一般都会重新编辑一下,或者用更少的文字手工输入一遍,有助于记忆,也会过滤掉冗余信息

之前不知道还有这个 :joy:

是的,我一般用作原始信息的积累,有点像作家整理素材的小本本。

hammerspoon 那段有点意思,感谢分享

同步这个事情,我有想过我如果有这个需求的话,就去买个raspberry pi,常年开着,然后用p2p的同步软件syncthing来同步,感觉很酷的样子

之前也考虑过,但是感觉NAT是个问题 :stuck_out_tongue:

Big Sur 下 Hammerspoon 调不起来捕获了, 有木有人遇到?