0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テーマは、探すものではなく、つくるもの —— Zed のテーマを自分で書いた話

0
Posted at

まず、数字から

Zed に入れているテーマ拡張は 26個です。

kanagawa、monokai、darcula、one-dark-ocean、neon-pulse、catppuccin、night-owl……名前を聞けば「ああ、あれか」となる定番はだいたい入っています。

そして、そのどれも使っていません

いま使っているのは、自分で書いた 3つです。

スクリーンショット 2026-08-10 100206.png

テーマセレクタ。一画面には収まりません。 スクロールしても、まだ続きます。

そもそも、Zed はテーマが少ない

自作に踏み切った理由の半分は、これです。両方のレジストリでテーマの数を数えてみました。

テーマ拡張数
VS Code Marketplace 19,380
Zed 公式レジストリ 580

約33分の1。 「なんとなく少ない気がする」ではなく、実際に33倍の差がありました。

VS Code なら、どんなにニッチな好みでも誰かが先に作っています。1万9千個もあれば、探せば見つかる。だから「テーマは探すもの」で成立していました。

Zed ではそれが成立しません。探しても、無いものは無い。

でも Zed のテーマは JSON 1枚です。書式は公開されていて、スキーマも配布されている。無いなら書けばいい。

この記事は、26個試して満足できず、最終的に自分で書くことになった話です。そして書いてみて初めて分かった、「テーマでできること」の境界線の話でもあります。

自作した3つのテーマ

テーマ 正体
Snow Owl VS Code 拡張 pxius.carbon-owl のライトテーマを、手作業で Zed 形式に移植
Warm Neon PyCharm/IntelliJ の WarmNeon.icls(Darcula ベース)を Zed に移植
Warm Owl 上記2つのハイブリッド。いま使っているのがこれ

Zed を使い始めたのは 7月24日でした。そして最初にやったのが、テーマ探しです。目についたものを片っ端からインストールして、切り替えて、また次を入れて——気づけば26個。

そのあと何が起きたかは、拡張機能フォルダの更新日時に残っていました。

  • 7月24日 — Zed を使い始める
  • 7月27日 18:29探すのをやめて、自分で書き始める
  • 7月28日 14:56 — 2本目(Warm Neon)
  • 7月28日 18:002つを混ぜて Warm Owl 完成
  • 7月28日 21:48 — 最終調整

探すのに3日、つくるのに1日半。

使い始めて3日で「良いテーマが無い」と結論を出し、4日目には自分のテーマで書いていたことになります。

なぜ既製テーマで満足できなかったのか

理由はシンプルで、欲しかった色が Zed に無かったからです。

VS Code で使っていた「Snow Owl」というライトテーマがあり、これが手に馴染んでいました。Zed でも同じ環境にしたくてテーマストアを探したのですが、無い。似た系統のライトテーマはあっても、あの色ではない。

自作テーマの README には、当時の自分がこう書いていました。

公式のテーマストアには存在しないため、Dev Extension として自分の環境にのみインストールする想定です。

無いなら書くしかない。そういう話です。

移植の壁:VS Code と Zed は、色の持ち方が違う

「VS Code の JSON があるんだから、変換すれば終わりでは?」

終わりませんでした。 ここがこの記事で一番技術的な部分です。

両者はシンタックスハイライトのモデルそのものが違います

VS Code Zed
定義キー colors / tokenColors style / syntax
色の割り当て単位 TextMate スコープ tree-sitter のキャプチャ

VS Code は entity.name.function.python のような文字列のスコープ名に色を付けます。一方 Zed は tree-sitter が構文木から拾った function keyword string といったキャプチャ名に色を付けます。

粒度も命名も対応関係もバラバラなので、機械的な1対1変換ができません。README にもそう書き残しています。

モデルが異なるため、完全な1対1変換はできません。色の意図を汲んで手動でマッピングしています。

結局、元テーマの JSON を眺めながら「この色は関数名を目立たせたいんだな」「これは型を落ち着かせたいんだな」と意図を推測して、Zed 側の対応するキャプチャに割り当てていく、という地道な作業になりました。

書き上げた JSON は、https://zed.dev/schema/themes/v0.2.0.json に対して ajv でスキーマ検証をかけて確認しています。手作業なので、タイポで色が1つ抜けても気づけません。

元テーマのバグも、そのまま持ってきた

移植中に、元テーマの定義でこういう箇所を見つけました。

ansiRed     : #bf8b56
ansiBrightRed: #bf8b56   ← 同じ

ansiMagentaansiBrightMagenta も同様。おそらく元テーマ側のミスです。

直すべきか迷ったのですが、そのまま踏襲して、README に理由を明記することにしました。

これは元テーマ側の値をそのまま反映した結果であり、移植時の誤りではありません。気になる場合は〜を好みの値に変更してください。

「移植」と名乗る以上、勝手に直すと元テーマとの差分が説明できなくなるからです。ただの手抜きでは? という気もしますが、記録が残っているので後から直すのは簡単です。

Warm Owl の中身:白い画面に、ネオンを一滴

そして本題のハイブリッドです。Snow Owl のライトな UI に、Warm Neon の刺激的な部分だけを移植しました。

extension.toml に自分で書いた説明がこうなっています。

Snow Owl (light) base UI and syntax, with Warm Neon's neon-green cursor, green selection, red comments, and dark terminal.

実際の値を並べるとこうです。

要素 何が起きるか
エディタ背景 #ffffff 純白
カーソル #00ff00 原色のネオングリーン
選択範囲 #348d3459 半透明の緑
コメント #cc3b3b italic 赤いイタリック
ターミナル背景 #404040 ライト UI なのにここだけ暗い

スクリーンショット 2026-08-10 102338.png

白いエディタ、赤いコメント、そして下に沈む暗いターミナル。

真っ白なエディタに、原色のカーソルが1本だけ光っている。 これがやりたかった絵です。

とくに気に入っているのが、赤いコメントです。

これ、既製テーマではまず見かけません。どれくらい見かけないのか気になったので、インストール済みの拡張に含まれる全81テーマのコメント色を集計してみました。

系統 割合
グレー系 66 81%
青/紫系 13 16%
緑系 1 1%
赤系 1 1%

8割がグレー。 そして唯一の赤系が、自分で移植した Warm Neon でした。

つまり、配布されているテーマ81個の中に、コメントが赤いものは1つもなかったわけです。

理由は分かる気がします。赤はエラー・削除・警告に予約された色で、一方コメントは慣習的に「目立たせない要素」。赤いコメントは、最も強い警告色を、最も控えめな要素に使う——定石を2つ同時に破っています。

でも、やってはいけない理由はどこにもありません多数決で色を決める必要もありません。 赤いコメントが good だと思ったから、そうしています。既製テーマを選んでいる限り、この1票は絶対に通りませんでした。

ターミナル:28色のうち25色は借り物

Warm Owl でいちばん Warm Neon 由来なのが、ターミナルの配色です。

3つのテーマのターミナル定義(28キー)を突き合わせると、こうなりました。

比較 一致数
Warm Owl ←→ Warm Neon 28個中25個
Warm Owl ←→ Snow Owl ほぼ全滅

Snow Owl のターミナルは背景 #261b37濃い紫に、紫と茶系のパレットでした。Warm Owl はそれを完全に捨てて、Warm Neon 側を丸ごと採用しています。

UI とシンタックスは Snow Owl、ターミナルは Warm Neon。 境界線がきれいに引かれていました。

Neon から持ってきて、いま効いている色がこのあたりです。

キー 何が起きるか
terminal.foreground #aedbb5 既定の文字色が白ではなく淡い緑
terminal.background #404040 ライト UI の中でここだけ暗い
ansi.magenta #ff00ff 原色マゼンタ。カーソルの #00ff00 と同じ思想

白い画面の下に、文字が緑で光る暗いターミナルが埋まっている。カーソルの原色グリーンと、ターミナルの原色マゼンタが呼応している。1枚の絵として繋がっています。

手で直した3色

面白いのはここからです。25個をそのまま使って、3個だけ変えていました

キー Warm Neon → Warm Owl 意図
ansi.bright_black #6b6b6b #fefcfc 実用の修正
ansi.white #d4d4d4 #d0b8a3 中間グレー → ベージュ
ansi.bright_white #ffffff #d8c8bb 純白 → ベージュ

bright_black は完全に実務的な判断です。 ANSI の bright_black は CLI ツールが「補足情報」「グレーアウトした行」に使う色ですが、背景が #404040 なのに #6b6b6b ではほぼ読めません。それを #fefcfc まで振り切って、いちばん明るい色にしています。名前が black なのに、実際にはほぼ白。

残り2つは真逆で、純粋な趣味です。 白系を2つとも、無彩色のグレーから暖色のベージュに寄せている。Warm Neon を、さらに warm にした。「Warm Owl」という名前が、ここで自分を説明しています。

実用の修正が1つ、趣味の修正が2つ。 自分のこだわりが、この3行にきれいに出ていました。

透過させたかっただけなのに

ここからが本題の後半、5回連続で間違えた話です。

暗いターミナルを透かして、後ろのデスクトップがうっすら見えるようにしたい。よくあるやつです。設定はとっくに書いてありました。

"terminal": {
    "background_appearance": "transparent",
}

でも、透けない。

Windows だしな、と思っていました。対応しきれていないんだろう、と。

間違い①:そのキー、存在しなかった

公式ドキュメントで terminal に指定できるキーを数えたら約25個。blinkingcursor_shapefont_familydock……

background_appearance は、その中にありませんでした。

Zed は知らないキーをエラーも警告も出さずに黙って無視します。だから、書いた本人は「設定した」つもりのまま、何も起きていないことに気づけません。

正しいキーは、公式のテーマスキーマ v0.2.0 で定義されている background.appearance でした。

  • アンダースコアではなくドット
  • 置き場所は settings.json ではなくテーマ JSON の style の中

つまり、キー名も場所も両方間違えていたわけです。

間違い②:直しても透けない

テーマ JSON に "background.appearance": "transparent" を追加。

まだ透けない。

terminal.background#404040ff ——末尾の ffアルファ完全不透明です。ウィンドウを透過モードにしても、ターミナル自体が不透明色で塗りつぶされていれば当然透けません。

間違い③:アルファを下げたら、ただ薄くなった

#404040ff#404040cc(80%不透明)に変更。

透けるのではなく、色が薄くなっただけでした。

原因はこれです。ターミナルパネルの下に、不透明な層が3枚重なっていました。

キー
4. 一番上 terminal.background #404040cc ← ここだけ下げた
3. パネル下地 panel.background #ffffffff
2. サーフェス surface.background #f3f6f6ff
1. ウィンドウ基底 background #f3f6f6ff
0. デスクトップ

透けた先はデスクトップではなく、真下にある panel.background の純白。暗いグレーが白と合成されて、ただ薄いグレーになる。

ガラスを1枚だけ入れても、その下が壁なら向こうは見えない。

間違い④:全部下げても、まだ薄い

じゃあ全部下げればいい。3層とも cc(80%不透明)に変更。

まだ薄いだけでした。

そしてここで、ようやく本当の原因に気づきます。

透過は足し算ではなく、掛け算。

80%不透明の板を3枚重ねたら、通る光は20%ではありません。

0.2 × 0.2 × 0.2 = 0.008

0.8%。 「透けない」のではなく、99.2%遮っていたのです。

そして透けた —— Windows は無罪だった

下層を cc ではなく 00(完全透明) にして、ようやく壁紙が見えました。

スクリーンショット 2026-08-10 102258.png

左のプロジェクトパネルとタブ周りが透けて、後ろのデスクトップが覗いている。ひとつ前の画像とまったく同じ画面です。

透けている先は、この壁紙です。

スクリーンショット 2026-08-10 101000.png

この山と湖が、パネル越しにうっすら見えていたわけです。

つまり、最初に疑った「Windows が対応していない」は完全な誤診でした。裏付けも取りました。

  • Zed のログ:Direct3D 11.1 で正常に GPU 合成されている
  • Windows 実装のソースは Transparent に対して SetWindowCompositionAttribute(hwnd, None, 2)ちゃんと呼んでいる
  • per-pixel の GPU 合成透過は 2025年4月のプルリクエストで対応済み

Windows が本当に苦手なのは transparent ではなく **blurred(すりガラス)**のほうでした。こちらは Windows の DWM API が「ぼかす」代わりに背景を約20%ズームアウトしてしまうという、別種の問題を抱えています。

5回間違えて、犯人は全部自分でした。

そして、透過が全部の粗を暴いた

透けたら透けたで、今度は今まで見えていたものが見えなくなりました

スクロールバーが消えました。 つまみの色が #3398db40 ——アクセントブルーのアルファ25%。不透明なパネルの上では薄く見えていたものが、壁紙の上では完全に溶けます。#3398dbcc(80%)まで濃くして、さらに本体より濃い青の輪郭を付けて解決しました。

プロジェクトパネルの文字も沈みました。 git のステータス色を全部強化することに。

状態 → 強化後
新規 #37ae6f #0e9455
変更 #d39e17 #c07f00
削除 #c13838 #cf1f1f
衝突 #d39e17 #c2185b
無視 #b3bec9 #94a3b0

この作業中に気づいたことがあります。「変更」と「衝突」が、元から同じ色(#d39e17)でした。

移植した時点から同じだったのに、透過に手を出すまで気づきませんでした。コンフリクトが起きても、ただの変更ファイルと同じ色で表示されていたわけです。衝突だけ深いマゼンタ #c2185b に分離しました。カーソルの #00ff00、ターミナルの #ff00ff と同じ「一点だけ原色を置く」思想の延長です。

淡い色で成立していた UI が、背景を透かした瞬間に全部破綻する。 透過のコストは、思っていたよりずっと広範囲でした。

テーマでできることの、境界線

ここまで来ると欲が出ます。

プロジェクトパネルで、変更したファイルの「行そのもの」に色を付けたい。 文字色ではなく、行の背景を塗りたい。

できませんでした。 テーマの問題ではなく、Zed 本体の実装がそうなっていません。

ファイル名の色を決めているのは、こういう関数です。

if git_status.conflict > 0        { Color::Conflict }
else if tracked.deleted > 0       { Color::Deleted }
else if tracked.modified > 0      { Color::Modified }
else if tracked.added > 0         { Color::Created }

これは文字色(ラベル)専用の関数で、返した色はファイル名にしか適用されません。

一方、行の背景色を決めているのはこちらです。

状態 使われるキー
通常 panel.background
ホバー element.hover
選択 element.selected

行背景は「選択状態」しか見ていません。git のステータスを一切参照していない。

テーマには modified.background というキーが存在します。でもプロジェクトパネルの行には配線されていない。テーマ JSON でどう頑張っても、受け側のコードが存在しないわけです。

TextMate と tree-sitter の話と同じ構図でした。テーマでできることの境界は、結局エディタ側の実装が決める。

最後の壁

もうひとつ、どうしても直らないものが残りました。

AI エージェントパネルのスクロールバーが、触れたときしか出ません。

色は濃くしました。設定も入れました。Zed 本体の既定設定ファイルを落としてきて、scrollbar が実在する場所を全部列挙もしました。今回は同じ間違いを繰り返さない。

キー 既定値
トップレベル scrollbar.show "auto"
project_panel / outline_panel / git_panel / terminal null(=トップレベルを継承)

エージェントパネル用のキーは、存在しませんでした。

じゃあトップレベルを "always" にすれば継承されるはず——と思って設定しても、エージェントパネルだけ変わらない

ソースを読みに行きました。GPUI のスクロールバーには、表示条件の指定方法が2つあります。

// 常に Auto 固定
pub fn new(show_along: ScrollAxes) -> Self { ... }

// settings.json と接続される
pub fn for_settings<S: ScrollbarVisibility + Default>() -> Scrollbars { ... }

project_panelterminal は後者を使っていて、null のときはトップレベルを継承する仕組みが明示的に実装されています。

そしてエージェントパネルのソース(約2万4千行)を検索した結果——その呼び出しが、1つも見つかりませんでした。

他のパネルには通っている「設定を継承する経路」が、ここだけ繋がっていない


テーマも、ターミナルの28色も、パネルの色も、スクロールバーの濃さも、全部自分で書き換えられました。JSON を1行直して zed: reload extensions を叩けば、その場で色が変わる。この距離の近さが気持ちよくて、ここまで来ました。

テーマが580個しかないことは、最初は不満でした。でも書いてみたら、探すより速かった。26個試すのに3日かかって、書き上げるのには1日半。少ないことは、思っていたほど問題ではありませんでした。

でも、エージェントパネルのスクロールバーだけは、どうやっても直らない。設定にキーが無く、実装にも経路が無い。ここから先は、JSON を何行書いても届きません。

——issue を立てるしかないのか。

おまけ:2つのテーマを持つことにした

透過は結局、日常使いには向きませんでした。パネルは全部透けるか全部不透明かの二択で、ターミナルだけ透かすことはできない。エディタも透かしてみましたが、コードが読みにくくて戻しました。

なので、2つ持つことにしました

テーマ 用途
Warm Owl 不透明。日常はこっち
Skeleton Warm Owl 透過版。気分が乗った日はこっち

透過はロマン、不透明は実用。 両方保存して、その日の気分でテーマセレクタから切り替えることにしました。

試行錯誤の値は、全部コメントで JSON に残してあります。

// エディタ透過の記録: #ffffff99(60%)で透けたが、コードが見えにくく ff に戻した。
// 再挑戦するなら editor.background / editor.gutter.background / toolbar.background の
// 3つを同じ値にする。目安: cc=80%(ほんのり), 99=60%(バランス), 66=40%(かなり透ける)。

またやるかもしれないので。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?