hsingko


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))))

  1. 这种数据类型其实就是 Association List ,又叫 alist ,它是一种被广泛使用的数据类型。李杀的这篇文章介绍得很好 ↩︎

  2. 其实已经有现成的 consult-notes 了,但它的代码比较复杂 ↩︎