写一个 Emacs 主题
目录
一直以来,我用的 Emacs 主题都是从 doom-themes 中挑选的。经过最初的筛选后,我逐渐变为只使用其中一个主题 —— doom-moonlight。但这也在我心存遗憾:如果我只使用其中一个主题,有必要安装整个 doom-themes 的主题包吗?
而后在长久的使用中,我逐渐发现 doom-moonlight 的主题有很多地方并不合我心意。例如 nerd-icon 的配色中本应使用紫色的地方却选取了偏粉色,例如 Emacs 的图标变成了粉色的图标(我还是更喜欢紫色的 Emacs 图标)。此外对于字体高亮,我更喜欢没那么“多彩”的高亮设置:只使用少量的颜色高亮各种关键部分即可,不需要函数名、变量名、类型名这些都采用不一样的颜色。
诚然,这些都可以通过在自己的配置文件中覆盖主题的设置,但这些零散的补丁让我产生一个念头:既然我与主题默认存在如此多的差异,为什么不干脆基于 doom-moonlight 自己写一个主题,完全采用个人喜好的设置,不就简单很多了?说干就干!
主题框架
事实上,绝大多数提供了多种主题的主题包,乃至只提供了浅色或暗色变体的主题,都内含了某个主题框架。其核心就是能够在多个主题间共享基础配置和代码。例如我用的 doom-themes,就通过 def-doom-theme 宏提供了一个主题框架。只需要你提供对应的颜色,就可以形成一个主题。在这个主题包中的所有主题,都是在这个框架中通过设置颜色和覆盖一些框架默认设置来制作的。
每个框架的使用方式都不一样,除了 doom-themes 的框架外,其他主题包比如 solarized-emacs 提供了 solarized-create-theme-file-with-palette ,只需提供 10 种颜色(2 种基础色 + 8 种辅助色),它就会自动生成一个主题文件,包含所有中间色调并对应的完整设置。
;; Copy from solarized-emacs github repo
(solarized-create-theme-file-with-palette 'light 'solarized-jellybeans-light
'("#202020" "#ffffff"
"#ffb964" "#8fbfdc" "#a04040" "#b05080" "#805090" "#fad08a" "#99ad6a" "#8fbfdc"))
(load-theme 'solarized-jellybeans-light t)
还有像是 modus-themes 则是能通过 modus-themes-common-palette-overrides 来在不修改主题的情况下,调整主题的颜色,从而能在运行时进行深度自定义。更妙的是,这个颜色调整能适用于该主题包的所有主题,也就是说,在使用 modus-themes-toggle 切换主题包内的主题时仍会保留用户的颜色调整。(针对特定主题的颜色调整也有对应的变量)
(setq modus-themes-common-palette-overrides
'((fg-main "#c8d3f5")
(bg-main "#212337")
(keyword magenta)))
如果已经使用了一些主题包,并且只是想在主题包上进行颜色调整和一些默认设置的覆盖,那么使用主题包提供的框架去写新主题无疑是更好的选择。
从零开始
然而我并未选择采用主题框架编写主题,主要还是因为我不愿为了只使用其中几个主题而安装一整个主题包。所以我采用了最传统的方法编写主题——用 deftheme 和 custom-theme-* 来从零开始写一个 Emacs 主题。
一个 Emacs 主题就是一系列 face 的自定义设置的集合。Emacs 的 face 就是文本的显示样式,包括前景色、背景色、粗体、斜体、下划线等等。因此写一个 Emacs 主题的传统方法就是用 deftheme 定义一个主题,然后用 custom-theme-set-faces 调整这个主题中各种 face 的设置,最后用 provide-theme 声明本文件提供了该主题的设置。
(deftheme my-theme "My custom theme.")
(custom-theme-set-faces
'my-theme
;; The `t` means "all display types" -- you can also specify different
;; colors for different displays (GUI vs 256-color terminal, etc.)
'(default ((t (:foreground "#c8d3f5" :background "#212337"))))
'(font-lock-keyword-face ((t (:foreground "#c099ff"))))
'(font-lock-string-face ((t (:foreground "#c3e88d"))))
;; ... 200+ more faces
)
(provide-theme 'my-theme)
好处很明显:我对所有的 face 都拥有完全的控制权,能在 Emacs 的能力范围内自由设定任意 face 的样式。而对于上述插件没有覆盖到的 face ,我也能自由地添加,而不用考虑潜在的冲突问题等。
坏处也很明显:我必须手动设定和维护所有的 face 。Emacs 内置的基本 face 就有几十个,而无论是 Emacs 内置的插件,还是社区的各种插件,都添加了自己的 face ,这就导致在编写主题时需要自行了解并设置的 face 数量众多。如果有所遗漏,主题在某些 UI 上很好看,而在另一些 UI 上却显得奇怪。我的主题仅涵盖我个人会用到的功能和插件的 face ,就已经有 700 多个了,可想而知社区主流的主题们为了适配各种插件可能要设定好几千个 face 了。
而维护这些 face 也需要耗费不少精力。例如使用的某个插件的新版本中添加了新的 UI 界面,不仅引入了新的 face ,还可能让你的主题出现奇怪的问题,必须调整来做适配。
就算你设置好了这些数量众多的 face 并决心维护好它们,你可能也会踩到新的坑:某些插件使用变量而不是 face 来设定颜色,比如 hl-todo 就是用 hl-todo-keyword-faces 变量来设定高亮的颜色1。解决方法就是用 custom-theme-set-variables 来设置这些变量,但你必须预先知道什么插件是用变量来设定颜色的。
这就是使用上述框架的好处了,这些框架往往都对各种各样的 face 都有默认的设置,只需要覆盖自己不喜欢的就行了。针对插件的更新,框架也会负责维护和添加对应的新 face 的默认值。而各种坑框架也已经先踩过了,不需要用户操心。正是这些框架的源码帮助我解决了不少坑和问题。
来点宏吧
社区早就注意到了从零开始写主题存在的各种问题,autothemer 通过 autothemer-deftheme 宏来让用户用更简单的方式来定义主题和避免问题。不仅将冗长的 custom-theme-set-faces 代码简化为了基于调色板的设置方法,还提供了为 GUI 和终端等不同显示界面设置备用颜色值等方便的功能。
;; Copy from solarized-emacs github repo
(autothemer-deftheme example-name "Autothemer example..."
;; Specify the color classes used by the theme
((((class color) (min-colors #xFFFFFF))
((class color) (min-colors #xFF)))
;; Specify the color palette, color columns correspond to each of the classes above.
(example-red "#781210" "#FF0000")
(example-green "#22881F" "#00D700")
(example-blue "#212288" "#0000FF")
(example-purple "#812FFF" "#Af00FF")
(example-yellow "#EFFE00" "#FFFF00")
(example-orange "#E06500" "#FF6600")
(example-cyan "#22DDFF" "#00FFFF"))
;; Specifications for Emacs faces.
;; Simpler than deftheme, just specify a face name and
;; a plist of face definitions (nested for :underline, :box etc.)
((button (:underline t :weight 'bold :foreground example-yellow))
(error (:foreground example-red)))
;; Forms after the face specifications are evaluated.
;; (palette vars can be used, read below for details.)
(custom-theme-set-variables 'example-name
`(ansi-color-names-vector [,example-red
,example-green
,example-blue
,example-purple
,example-yellow
,example-orange
,example-cyan])))
这确实是针对手写主题来说非常方便的宏,遗憾的是,我是在写完主题后才发现这个包2。如果再次从零开始手写一个主题,我会考虑用 autothemer 等方案,以大幅节省时间和精力。
总结
从零开始写一个 Emacs 主题对我而言是个非常有意思的过程。为各个 face 挑选美观一致的颜色,看着粗糙的默认外观一点点好看起来,给我十足的满足感。但不可否定的是,这确实十分耗费时间和精力。每次发现仍有一些 face 未被覆盖时,都会令我头疼。
如果你喜欢已有的主题但需要大量的修改,除非像我一样有某种强迫症,否则我还是会推荐使用主题框架。毕竟这些框架往往都久经社区考验,很可能改着改着就发现还不如原来的好看。
如果你真要从零开始手写一个主题,我会推荐使用上述的宏等解决方法。节省下的大量时间和精力可以用来专注于让你的主题更好看。
当然,最省心的就是进入对各种默认配置都满意,懒得折腾,“It works”就行的佛系状态,这确实是令人羡慕的状态了。