Emacs Advent Calendar 2020 の 9 日目の記事です。
おととしの Advent Calendar でカラーテーマについての話を書いたのですが、その後2年、実際に自分で使ってみて色々と考えが整理された&実装も進化したので改めて紹介させてください。
当時の記事と重なる箇所もありますが、ご了承ください mm
tl;dr
「色を持たない」カラーテーマによって、 Emacs の外観を簡単に、一貫性を保ってコントロールできるようにしました。
カラーテーマの責務
Emacs の Basic Faces
Emacs には Basic Faces と呼ばれる、いくつかの特別な face があります。
defaultbolditalicbold-italicunderlinefixed-pitchfixed-pitch-serifvariable-pitchshadowlinklink-visitedhighlightmatchisearchlazy-highlighterrorwarningsuccess
マニュアルによれば、他の Face たちはこれら Basic Faces を継承して定義することが推奨されており、「Basic Faces をカスタマイズするだけで Emacs 全体をカスタマイズできる」ことを理想としているように読めます。
しかしその後リッチなテーマ機構が実装されたことなどからも、この思想には限界があったように思えます。
なにがしんどいのか
私の考える Emacs の face の問題 (限界) は以下の二点です:
-
Basic Face が圧倒的に足りていないこと
-
「意味」の face と「見た目」の face が混在していること
1 について、いろいろなパッケージがそれぞれ独自に、 Basic Faces を継承しない face を定義していることから、 Basic Faces は現実のアプリにはかなり不足していることがわかると思います。
たとえば組み込みのシンタックスハイライトですら大量に独自 face を定義しています:
font-lock-comment-facefont-lock-comment-delimiter-facefont-lock-string-facefont-lock-doc-facefont-lock-keyword-facefont-lock-builtin-facefont-lock-function-name-facefont-lock-variable-name-facefont-lock-type-facefont-lock-constant-facefont-lock-warning-facefont-lock-negation-char-facefont-lock-preprocessor-facefont-lock-regexp-grouping-backslashfont-lock-regexp-grouping-construct
さらに悪いことには、これらの face たちは「その見た目のためだけに」また別のパッケージでさまざまに利用されています。
たとえば Emacs でディレクトリを開いたときの画面 (dired) では、ディレクトリのパスが表示されるヘッダ行に font-lock-type-face が使われています。 face 名からして本来は「型の名前をハイライトするための face」だったものを、ここではその「見た目だけ」を拝借するために使用していると考えられます。
これが 2 の問題で、「名前が用途を表している face」を、見た目のためだけに用途外の場所で使用してしまうと、全体の一貫性がなくなってしまいやすく、結果的にカラーテーマの実装もしづらくなってしまうと感じます。
たとえば dired に似た、たくさんの項目が並んだリスト状の画面を表示するための別のモード tabulated-list-mode では、ヘッダの表示に tabulated-list-fake-header というまた別の face が使われていて、これはこれで font-lock-type-face とは別の見た目をしていたりします。
カラーテーマのもう一つの責務
現状の Emacs では、このようなチグハグ状態をなんとかするのもカラースキームの責務になってしまっているように感じています。各々のパッケージが自由に独自 face を定義して、そのルールに一貫性があまりないので、カラーテーマ側であらゆるパッケージの face の色をいい感じに設定して回る必要があります。
たんにエディタの画面全体の色合いを衣替えしたいだけの場合、これはちょっと大変すぎかなと思います。
elemental-theme の設計
そこで私は、この「カラーテーマのもう一つの責務」だけを上手にこなして、カラーパレットの選定はユーザーに開いたままにしておく、「色を持たない」カラーテーマ elemental-theme を実装しました(便宜上、デフォルトのスタイルは定義されていますが)。
以下、その設計思想を紹介したいと思います。
Elemental Faces
elemental-theme はまず、「見た目だけ」を表す face を必要十分な数だけ定義します。
elemental-theme ではこれらを elemental (根本の, 要素の) face と呼んでいます。 Atomic Design の atoms みたいなイメージです。
core
-
default(built-in) -
cursor(built-in)
濃淡
elemental-bright-bg-faceelemental-brighter-bg-faceelemental-bright-fg-faceelemental-dark-fg-faceelemental-darker-fg-face
アクセント
elemental-accent-fg-1-faceelemental-accent-fg-2-faceelemental-accent-fg-3-faceelemental-accent-fg-4-face
他のすべての face はこれらの elemental な face を継承して定義されるので、 elemental-theme で使われるスタイルはこれで全部です。 逆に、ここを調整するだけで、 Emacs 全体の見た目を一貫性を保ってコントロールできるというのがこのカラーテーマのコンセプトになります。
このあたりはおととしの記事時点でも同じような考えでしたが、「必要十分な」face のセットという点については考察が進んで、実装も変わっています。
命名について
名前に -fg- とつく face は主に単語などのハイライトに、 -bg- とつく face は主に一定の大きさを持ったエリア全体のハイライトに使われます。そのため、 -fg- face には前景色を、 -bg- face には背景色を設定するとうまくいくことが多いです。また -fg- と -bg- face は適宜組み合わせて使われるので、互いにコンフリクトしないようにする必要があります。
アクセント色については、 -1- から順に高い頻度で使われます。たとえば、 accent-fg-1 にはそのカラーテーマのメインとなるアクセント色、ブランド色を適用するといい感じになることが多いです。
色相 face
一部の色については UI 上で特別な意味を持つことがある(たとえば、エラーは暖色系、成功は寒色系)ので、専用の face を設けています。色数を減らしたいテーマでは、他の elemental face に使われている色の中から適当な色を選んで選んで流用してもよいとしています。
elemental-red-fg-faceelemental-blue-fg-faceelemental-green-fg-faceelemental-orange-fg-face
Semantic Faces
elemental-theme は次に、「用途だけ」を表す face を必要十分な数だけ定義します。ここが一番大きなアップデートです。
elemental-theme ではこれらを semantic (意味的な) face と呼んでいます。
basic
elemental-cautionelemental-diminishedelemental-highlightelemental-inappropreateelemental-incorrectelemental-keyelemental-matchelemental-match-interactiveelemental-referenceelemental-selected
増減、追加削除
elemental-diff-addedelemental-diff-changedelemental-diff-refine-addedelemental-diff-refine-changedelemental-diff-refine-removedelemental-diff-removed
ファイル
elemental-file-executableelemental-file-nameelemental-file-special
アラート
elemental-indicator-errorelemental-indicator-infoelemental-indicator-successelemental-indicator-warning
階層構造
elemental-level-1elemental-level-2elemental-level-3elemental-level-4elemental-stage-1elemental-stage-higherelemental-stage-negative
マークアップ言語の構文
elemental-markup-codeelemental-markup-table
プログラミング言語の構文
elemental-syntax-builtinelemental-syntax-literalelemental-syntax-symbolelemental-syntax-type
その他 UI 部品
elemental-ui-componentelemental-ui-component-diminishedelemental-ui-component-transparentelemental-ui-ghostelemental-ui-prompt
これらの face はすべて elemental face を継承して定義されており、ここで「見た目」のパレットと「意味」のパレットを分離しています。elemental-theme では、各パッケージの face に色を塗る際、 elemental face を直接使うことはいっさいせず、必ず「意味」のパレットから選ぶようにします。
たとえば region (バッファの選択範囲)、 ido-first-match (ミニバッファのメニューで選択されている項目)、 company-tooltip-selection (選択されている補完候補) などはどれも「選択されている」という概念を表しているので、すべて elemental-selected semantic face を継承させます。
これは一貫性を保ちつつ対応パッケージを増やしていくために役立ちます。
また elemental-theme では semantic face のカスタマイズも禁止していないので、たとえば同じ意味を持つ要素の見た目をまとめて変更したい場合は、ユーザーはこのレイヤーに手を入れることもできます。
ところで semantic face は elemental face よりもたくさんあるので、いくつかの semantic face は同じ見た目になる場合があります。また対応プラグインを増やせば増やすほど、そのようなケースは増えていくと思われます。そこで elemental-theme では以下のポリシーで sematnic face を定義していくことにしています。
1. 一貫性
同じ機能を持つ部分はすべて同じ見た目になるようにします。
もし新しいパッケージをサポートする際は、極力既存の「意味」のパレットから適切なものを選ぶようにします。ただし、 適切な「意味」の face がパレットにない場合は、無理に他のもので代用することはせず、新たに追加することにします。
2. 区別できること
同じ見た目の要素が異なる機能を持つことを、次の場合に限って認めます: それらが同じバッファや UI 部品の中で使われない場合。
たとえば elemental-markup-table と elemental-syntax-literal は(デフォルトでは)同じ見た目をしています。これらはそれぞれ「マークアップ言語 (markdown, org など) の表」と「プログラミング言語の定数」をハイライトするために使われる face なので、一つのバッファに同時に出現することはありません。したがって、これらは同じ見た目をしていても問題ありません。
一方、たとえば elemental-markup-code (マークアップ言語の引用・コード部分) を elemental-markup-table と同じ見た目にするのはまずいです。なぜなら、どちらもマークアップ言語のシンタックスハイライトに使用されるので、同じ見た目をしていると区別できなくなってしまいます。
まとめ
face に「見た目」と「意味」それぞれの抽象化レイヤーを導入することで、 Emacs の face 周りのツラみを解決しようとしてみました。またその仕組みをいい感じに維持するための運用ポリシーを考えてみました。
今のところ快適に使えているので、よかったら試してみてください! (アイデアだけ他のカラーテーマに援用するのも歓迎です)
おまけ
elemental-theme はカラーテーマジェネレータではなく「継承関係のよく整理されたただのカラーテーマ」なので、 set-face-foreground などでリアルタイムにいじることもできます。
悪用するとゲーミング Emacs (?) などもつくれます
ゲーミング LISP かなり最悪感ある pic.twitter.com/2anF9gnY1X
— ぜろけー🎄 (@zk_phi) November 19, 2020





