Vompeccc 案例研究:在 Emacs 中把 Spotify 做成纯 ICR

Hacker News Top 工具

摘要

深入案例研究,展示作者如何借助 VOMPECCC 补全框架在 Emacs 内构建 Spotify 客户端,演示 Consult、Marginalia 与 Embark 等包的模块化集成。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/04/22 21:15

# VOMPECCC 案例研究:在 Emacs 中以纯 ICR 方式使用 Spotify 来源:https://www.chiply.dev/post-vompeccc-spot ## 目录 - 1\. 关于 emacs completion(https://www.chiply.dev/post-vompeccc-spot#about) - 2\. 演示 consult marginalia embark(https://www.chiply.dev/post-vompeccc-spot#demonstration) - 3\. spot 的解剖 structure modularity(https://www.chiply.dev/post-vompeccc-spot#anatomy) - 4\. 候选者即通用货币 candidates(https://www.chiply.dev/post-vompeccc-spot#candidates-as-currency) - 5\. Consult:定义搜索面 consult async narrowing(https://www.chiply.dev/post-vompeccc-spot#consult) - 6\. 为什么需要缓存?async rate limits(https://www.chiply.dev/post-vompeccc-spot#cache) - 7\. Marginalia:把候选者升级为知情选择 marginalia(https://www.chiply.dev/post-vompeccc-spot#marginalia) - 8\. Embark:动作层 embark composition(https://www.chiply.dev/post-vompeccc-spot#embark) - 8\.1\. 组合:当一个动作打开另一个搜索 composition chaining(https://www.chiply.dev/post-vompeccc-spot#composition) - 9\. 集成点:spot-mode modularity hooks(https://www.chiply.dev/post-vompeccc-spot#spot-mode) - 10\. 反事实:没有 VOMPECCC 的 spot 会长什么样(https://www.chiply.dev/post-vompeccc-spot#counterfactual) - 11\. 这对底层意味着什么 substrate platform(https://www.chiply.dev/post-vompeccc-spot#substrate-takeaway) - 12\. 把模式推广到 Spotify 之外 generalization pattern(https://www.chiply.dev/post-vompeccc-spot#generalization) - 13\. 结论(https://www.chiply.dev/post-vompeccc-spot#conclusion) - 14\. 太长不看(https://www.chiply.dev/post-vompeccc-spot#tldr) ## 1\. 关于 emacs completion 这是 Emacs 补全系列的第三篇。第一篇(https://www.chiply.dev/post-icr-primer)论证了**增量补全读取(ICR)**不只是 UI 便利,而是接口的**结构性**特征;Emacs 是少数把补全暴露为可编程**底层平台**而非封闭 UI 的环境。第二篇(https://www.chiply.dev/post-vompeccc)把该平台拆成八个包(统称 VOMPECCC),分别解决完整补全系统的六个正交关注点。 本文用具体代码展示**如何基于 VOMPECCC 构建**,逐行剖析 `spot`(https://github.com/chiply/spot)——我在 Emacs 里实现的纯 ICR Spotify 客户端。 文中把 `spot` 对 VOMPECCC 的使用称为**垫片(shim)**。整个包约 1100 行非空非注释 Lisp¹。其中 635 行是任何 Spotify 客户端都绕不开的基础设施:OAuth 刷新、HTTP 传输、缓存搜索、当前播放状态、配置、播放控制……**垫片是剩下的 493 行**,分布在三个文件(`spot-consult.el`、`spot-marginalia.el`、`spot-embark.el`),只做一件事:把候选者喂给 Consult(https://github.com/minad/consult),让 Marginalia(https://github.com/minad/marginalia)加注解,再用 Embark(https://github.com/oantolin/embark)绑动作。 `spot` 自己**没有 UI**:没有 tabulated-list 缓冲区、没有自定义键图、没有渲染代码。所有交互面——搜索提示、候选列表、注解、动作菜单——都是从补全底层**租**来的。本文只讲代码:不罗列功能(等发 Melpa 再吹),只看**在 VOMPECCC 之上代码如何拼装**,并给出实拍片段与源码对照。前两篇讲“为什么”和“是什么”,这篇讲“怎么做”。 ## 2\. 演示 consult marginalia embark 先上任务:我找不到一首 J Dilla 的歌,只记得歌名里有 *don't*。下面整篇都围绕这条 30 秒录屏展开,建议先看完再继续。 右上角 tab-bar 会实时显示我按下的键与命令。视频可暂停、 scrub、全屏、画中画。 流程: 1. 执行 `spot-consult-search`,输入 `j dilla`。每敲一次键都异步查 Spotify Web API,结果流式涌入 minibuffer——这是 **Consult**。我的 Vertico² 把候选纵向排列,方便读元数据。 2. 用 Spotify 查询参数拓宽结果集:`--type=track --limit=50` 等。API 按类型分页,Consult 照单全收。 3. 输入英文逗号 `,`(`consult-async-split-style` 分隔符),切换为**本地 ICR**。逗号前仍是远程查询;逗号后只在前端过滤,不再请求 API。 4. 输入 `dont`(无撇号)。默认字面匹配,0 结果——歌在库里,但模式对不上。 5. 退格,加前缀 `~`(Orderless³ 的模糊匹配调度符)。`~dont` 立即命中 "Don't Cry" 等——无需重查,只换了匹配算法。 6. 追加 `@donuts`(Orderless 的“注解列匹配”调度符)。把候选收窄到**注解**里出现“donuts”的曲目(即 Dilla 专辑 *Donuts* 里的歌),尽管歌名本身没有该词。 7. 选中后 `embark-act` → `P` 播放。`P` 绑定到 `spot-action--generic-play-uri`,从候选的 `multi-data` 属性取 URI,PUT 到 Spotify 播放器。 全程只用了 VOMPECCC 三件套:Consult(异步流 + 分隔符切本地)、Marginalia(被 `@` 过滤的注解列)、Embark(动作菜单)。**这些能力无需自己写**,`spot` 只需喂数据。 ## 3\. spot 的解剖 structure modularity 文件与职责一一对应,镜像 VOMPECCC 的模块化。 | 文件 | 职责 | 底层包 | 行数 | |---|---|---|---| | `spot-auth.el` | OAuth2 + 刷新定时器 | 无 | 65 | | `spot-generic-query.el` | HTTP 同步/异步、错误暴露 | 无 | 88 | | `spot-search.el` | 带缓存的搜索 | 无 | 100 | | `spot-generic-action.el` | 播放控制 | 无 | 51 | | `spot-mode-line.el` | 正在播放显示 | 无 | 115 | | `spot-var.el` | 配置变量 | 无 | 127 | | `spot-util.el` | alist/hash 转换、候选加属性 | 胶水 | 52 | | `spot-consult.el` | 7 个异步 Consult 源 + consult--multi 入口 | Consult | 194 | | `spot-marginalia.el` | 按内容类型的注解函数 | Marginalia | 159 | | `spot-embark.el` | 按内容类型的键图与动作 | Embark | 140 | | `spot.el` | spot-mode:注册表 + 定时器开关 | 集成 | 37 | | **总计** | | | **1128** | 垫片就这三文件(194+159+140=493 行)。其中 105 行是 7 路并行三元组(每类型一个源定义 + 历史变量 + 补全函数),可宏压缩到 ~30 行,但为了避免掩盖 VOMPECCC API 原貌,我故意手写。 ## 4\. 候选者即通用货币 candidates 先看让三层无缝协作的 12 行函数: ```elisp (defun spot--propertize-items (tables) "给哈希表 TABLES 加属性,用于补全显示。 每表需含 `name' 与 `type' 键。显示名按 `spot-candidate-max-width' 截断; 完整数据挂 `multi-data' 属性。" (-map (lambda (table) (propertize (spot--truncate-name (ht-get table 'name)) 'category (intern (ht-get table 'type)) 'multi-data table)) tables)) ``` 每个候选是**字符串**(Spotify 名称)+ 两个**文本属性**: - `category`:album/artist/track… Emacs 补全元数据协议用它路由到对应注解器与键图。Marginalia 和 Embark **互不通信**,却总能对得上,因为读的是同一标准属性。 - `multi-data`:整张 API 回表,Marginalia 拿它画边注,Embark 拿它做播放、导航、加列表。 > Marginalia 与 Embark 永不见面;它们只读同一属性,就够了。 集成面 = **1 个字符串 + 2 个属性**。其余(异步、过滤、注解、动作菜单)全由 VOMPECCC 按属性分发。这就是“底层”而非“UI”:UI 会给你一个小部件让你绑数据;底层给你**通用货币**(带标准属性的候选),能随意拼装工具。 ## 5\. Consult:定义搜索面 consult async narrowing Consult 是 `spot` 的大门,白送异步流、多源归并、narrowing 键、历史等。下面是一个源定义: ```elisp (defvar spot--consult-source-track `(:async ,(consult--dynamic-collection #'spot--consult-completion-function-consult-track :min-input 1) :name "Track" :narrow ?t :category track :history spot--history-source-track) "Spotify 曲目 Consult 源。") ``` 关键键: - `:async`:候选流。`consult--dynamic-collection` 包装一个函数,输入当前 minibuffer 字符串,返回候选列表;Consult 自己管防抖与缓存。`:min-input 1` 防空查。 - `:narrow ?t`:在 consult--multi 里按 `t` 只留该源。 - `:category track`:给候选打 category 属性,供 Marginalia/Embark 路由。 Consult 的**两层异步过滤**:外部 API 做重过滤,前端再做轻过滤。`spot` 只负责“给查询 → 返候选”,其余 Consult 全包。

相似文章

推荐一下……理解 Emacs 的模式

Lobsters Hottest

文章解释了 Emacs 的架构模式,重点介绍了通用缓冲区数据模型和增量补全读取(ICR),并将其比作玫瑰的根与花瓣。文章强调了 Emacs 如何统一界面并通过 Elisp 实现可扩展性。

Vim中的Lisp(2019)

Hacker News Top

详细比较了Slimv和Vlime这两个用于交互式Lisp编程的Vim插件,涵盖安装、功能及推荐。

ytr:Emacs 上的 YouTube 电台

Hacker News Top

一款名为 ytr 的新型 Emacs 软件包可将 YouTube 音频流作为电台小部件播放,基于 mpv 和 yt-dlp 驱动,并已在 GitHub 上提供。

软件界的Emacs化

Hacker News Top

作者讲述了在终端中阅读 Markdown 的烦恼,并描述了如何使用 Claude 快速构建一个自定义的 macOS Markdown 查看器(MDV.app),展示了 AI 如何让人能够迅速创建个人软件工具。

插件案例研究:Pluggy

Eli Bendersky

一篇博客文章,探讨了Pluggy——这是一个最初源自pytest的Python库,用于构建插件系统。内容包括其工作原理以及如何将其与一个玩具级HTML转换工具配合使用。