Emacs Consult 入门:以实现 consult-buku 为例
buku 是一个极简的网页书签管理工具,所有操作都可以通过命令行完成,唯一的缺点是搜索界面不够直观,在终端上可以通过 fzf 管道弥补这一点,但在 Emacs 中我们也可以做到,甚至实现得更好。
buku 的书签存储在一个 sqlite 数据库中,所以我们要做的就是使用 emacs 查询出数据,并调用 consult 的 API 进行检索。这篇文章将简要介绍如何实现类似的功能,它也可以作为一个使用 consult--read
API 的 demo ,我将通过几个简单的例子让你学会使用它。
功能 demo 代码如下:
(let* ((buku-entries (sqlite-select (sqlite-open "~/.local/share/buku/bookmarks.db")
"select metadata as title, url from bookmarks"))
(candidates (mapcar (lambda (row)
(cons
(nth 0 row)
(nth 1 row)))
buku-entries))
(url (consult--read candidates
:lookup #'consult--lookup-cdr)))
(browse-url url))
代码解释
代码可以分为四个部分:
- 使用 emacs 内置的 sqlite 查询数据库
- 使用
mapcar
对数据进行处理 - 使用
consult--read
查询数据,得到书签的网址 - 使用
browse-url
打开网址
前两步的代码都很直观,其中 emacs 内置对 sqlite 的支持是在 29 版本之后。此外 select
返回结果的格式是是一个 list ,可以使用 (nth ...)
获取前两个元素。
consult--read
是该功能的核心,它的使用方法只能看代码中的注释。然而注释在我看来实在太复杂,有很多参数在这个场景中其实并没有必要。
可以通过下面几个简单的例子来学会使用这个 API 。
第一个例子
(consult--read (list "Alice" "Tom" "Jack"))
可以看到 consult--read
支持字符串数据,而它的返回结果就是选中的字符串。这初看上去没有什么用,但实际上只要搭配其它函数就能实现强大的效果。比如 font-family-list
是一个获得系统安装字体名称的函数,在 emacs 配置字体时我们常常需要输入字族的名称,如果可以搜索的话会很方便,而通过 consult--read
可以非常简单地实现这个功能:
(defun consult-family-name ()
(interactive)
(insert (consult--read (font-family-list))))
第二个例子
现在假设有另一个场景,我们有如下数据1,分别包含了人名与相应的年龄,我们需要搜索人名然后获得对应的年龄。
(setq my-data (list
(cons "Alice" 18)
(cons "Tom" 23)
(cons "Jack" 38)))
如果直接使用 (consult--read my-data)
搜索结果返回的仍然是姓名而不是年龄,这时我们就需要用到 :lookup
:
(consult--read my-data
:lookup #'consult--lookup-cdr)
通过执行这个方法,我们可以猜到这 consult--lookup-cdr
的功能是取出 (cons "Alice" 18)
中 cdr
槽位的数据,也就是我们想要的年龄 18 。
在最初的 demo 中就使用了这个方法,取出了网页标题对应的 url 网址。
最后的 browse-url
很简单,就是调用系统默认的浏览器访问这个网址。
包装成交互函数
为了让这个功能可以通过 M-x
调用,我们需要把它包装成一个交互函数:
(defcustom buku-db-file "~/.local/share/buku/bookmarks.db"
"the buku bookmark database file")
(defun consult-buku ()
"consult buku bookmarks"
(interactive)
(let* ((buku-entries (sqlite-select (sqlite-open buku-db-file)
"select metadata as title, url from bookmarks"))
(candidates (mapcar (lambda (row)
(cons
(nth 0 row)
(nth 1 row)))
buku-entries))
(url (consult--read candidates
:lookup #'consult--lookup-cdr)))
(browse-url url)))
未来可以增添的功能
- 使用
text-property
美化候选列表 - 预览网页的信息,比如展示视频网站的缩略图
- 能够查询 tag 和 description 字段
- 缓存
目前已经实现美化与搜索 tag, domain 的功能,代码参考这个 gist 。
效果截图:
Bonus 1: 查询 firefox 历史记录
firefox 查询历史记录不太方便,用本文中的方法,也可以类似地实现在 emacs 进行查询。
代码:
(setq firefox-his-db "~/.mozilla/firefox/<profile>/places.sqlite")
(defun consult-firefox-history ()
(interactive)
(let* ((db-tmp (make-temp-file "consult-f-"))
(results (progn
(copy-file firefox-his-db db-tmp t)
(sqlite-select (sqlite-open db-tmp)
"select title, url from moz_places")))
(candidates (mapcar (lambda (row)
(cons (nth 0 row)
(nth 1 row)))
results))
(selected (consult--read candidates
:lookup #'consult--lookup-cdr)))
(browse-url selected)))
需要注意的是这里拷贝了 sqlite 数据库,这样可以避免浏览器运行时 lock 数据库。这个代码有很大优化的空间,因为每次查询都会重新拷贝一份数据库,对性能和空间都是浪费,但临时使用的话是足够了。
Bonus 2: 一行代码实现 consult-denote
再一次赞叹 prot 的漂亮代码,让我们可以很轻松地实现这个功能2:
(find-file (consult--read (denote-directory-files)))
但这个实现的问题是候选项中带有完整的路径名,很不方便,所以可以先获得相对路径名,最后得到结果再 expand :
(defun consult-denote-file-prompt ()
(expand-file-name
(consult--read (mapcar (lambda (f) (file-relative-name f denote-directory))
(denote-directory-files)))
denote-directory))
(defun consult-denote ()
(interactive)
(find-file (consult-denote-file-prompt)))
denote 自带的插入 link 的工具并不好用,参考 denote-link
的源代码,将其中 denote-file-prompt
替换成 consult-denote-file-prompt
就能实现升级。
(defun consult-denote-link (file file-type description &optional id-only)
(interactive
(let ((file (consult-denote-file-prompt)) ;; <====== Hey, I'm here
(type (denote-filetype-heuristics (buffer-file-name))))
(list
file
type
(denote--link-get-description file type)
current-prefix-arg)))
(let* ((beg (point))
(identifier-only (or id-only (string-empty-p description))))
(insert
(denote-format-link
file
(denote-link--file-type-format file-type identifier-only)
description))
(unless (derived-mode-p 'org-mode)
(make-button beg (point) 'type 'denote-link-button))))
-
这种数据类型其实就是 Association List ,又叫 alist ,它是一种被广泛使用的数据类型。李杀的这篇文章介绍得很好 ↩︎
-
其实已经有现成的 consult-notes 了,但它的代码比较复杂 ↩︎