<?xml version="1.0" encoding="UTF-8"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>Yukimemi&#39;s Blog</title>
      <link>https://yukimemi.pages.dev/</link>
      <atom:link href="https://yukimemi.pages.dev/feed.xml" rel="self" type="application/rss+xml"/>
      <description>Thoughts, code, and life.</description>
      <lastBuildDate>Sat, 08 Aug 2026 12:00:00 GMT</lastBuildDate>
      <language>ja</language>
      <generator>Zola</generator>
      <item>
          <title>Vim風操作と即時P2P同期を備えたローカルガントチャート yaiba</title>
          <link>https://yukimemi.pages.dev/posts/yaiba/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/yaiba/</guid>
          <pubDate>Sat, 08 Aug 2026 12:00:00 GMT</pubDate>
          <description>Vim風のキーバインドで高速に操作でき、P2Pによる即時同期に対応したローカルファーストのガントチャート・ToDo管理ツール yaiba を作りました。シングルバイナリ構造、Office/Superモード、イナズマ線、MCP対応などの特徴を紹介します。</description>
          <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/yukimemi/yaiba/main/assets/logo.svg" alt="yaiba — vim-flavoured todo &amp; gantt, peer to peer" /></p>
<p><strong>刃 — シングルバイナリで動作し、サーバーなしで即時P2P同期する Vim 風ガントチャート・ToDoプランナー</strong></p>
<p><img src="https://raw.githubusercontent.com/yukimemi/yaiba/main/assets/demo.gif" alt="yaiba demo" /></p>
<p>Vim のモーダル操作で高速に入力・編集でき、タスク間の依存関係も定義できるローカルファーストなガントチャート・ToDo管理ツール <strong>yaiba (刃)</strong> を Rust で作りました。</p>
<blockquote>
<p>刃 — <em>the blade</em>. A vim-flavoured todo and gantt planner that runs as a single local binary and syncs directly between people.</p>
</blockquote>
<a href="https://github.com/yukimemi/yaiba" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/yaiba: 刃 — vim-flavoured todo &amp; gantt planner. One local binary, embedded UI, peer-to-peer sync over iroh.</div><div class="link-card-description">刃 — vim-flavoured todo &amp; gantt planner. One local binary, embedded UI, peer-to-peer sync over iroh. - yukimemi/yaiba</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/fbe7e5f61a2bfee6673a90c02f77d470def99122c86527d6dbc1bdec912dcdfc/yukimemi/yaiba')"></div></a>
<h2 id="なぜ作ったのか">なぜ作ったのか</h2>
<p>タスク管理ツールを使う際、いつも以下のようなちょっとした不満がありました。</p>
<ol>
<li><strong>ターミナルツール</strong>：タイピングやキー操作は高速だが、全体のタイムラインや依存関係を直感的に俯瞰できるガントチャートを描くのが難しい。</li>
<li><strong>Webベースのプランナー</strong>：美しいガントチャートを描画してくれるが、操作のたびにマウスに手を伸ばす必要があり、データも他人のサーバーに預けなければならない。</li>
<li><strong>サーバー管理とアカウントの負担</strong>：チームでリアルタイム共有しようとすると、サーバーのセルフホストやアカウント作成・認証の手間が発生する。</li>
</ol>
<p>「<strong>キーボード主体で快適に操作でき、タスクの依存関係とタイムラインが美しく描画され、かつサーバー不要でP2P同期できるツール</strong>」が欲しいという思いから開発したのが <strong>yaiba</strong> です。</p>
<hr />
<h2 id="yaiba_の主な特徴">yaiba の主な特徴</h2>
<h3 id="1._キーボードがそのままインターフェース（Vim_モーダル操作）">1. キーボードがそのままインターフェース（Vim モーダル操作）</h3>
<p><code>yaiba</code> は Vim の操作体系を全面的に採用しています。</p>
<ul>
<li><code>o</code> / <code>O</code> で上下に新しいタスクを開いて即入力（<code>Enter</code> または <code>Esc</code> で確定）</li>
<li><code>Space</code> で完了（または <code>s</code> で <code>todo</code> → <code>doing</code> → <code>done</code> をトグル）</li>
<li><code>dd</code> でタスク削除、<code>u</code> / <code>Ctrl+r</code> で Undo / Redo</li>
<li><code>J</code> / <code>K</code> でタスクの上下移動（親の階層レベルに合わせて自動でインデント）</li>
<li><code>&gt;&gt;</code> / <code>&lt;&lt;</code> でタスクの階層ネスト / 押し出し</li>
<li><code>+</code> / <code>-</code> で所要日数（duration）の増減、<code>.</code> / <code>,</code> で開始日の移動</li>
<li><code>D</code> で依存関係の追加（先行タスクを指定）、<code>X</code> で解除</li>
</ul>
<p>キーボードから手を離さずに、プロジェクトのタスク追加・並び替え・スケジュール調整が完結します。</p>
<h3 id="2._依存関係とクリティカルパスの自動計算">2. 依存関係とクリティカルパスの自動計算</h3>
<p>単なるビジュアル的な矢印ではなく、Finish-to-Start（完了後開始）の DAG（有向非巡回グラフ）として依存関係を計算します。</p>
<ul>
<li>Forward/Backward pass により、各タスクの「最早開始日」「余裕日数（Slack）」を算出。</li>
<li><strong>クリティカルパス（Critical Path）</strong> を自動検出し、ハイライト表示。「このプロジェクトが実際にいつ終わるのか」を提示します。</li>
<li>後続タスクとのラグ日数指定（<code>:dep 3 +5</code> で5日間の余裕を設定など）や、同日開始（<code>:dep 3 +0</code>）にも対応。</li>
</ul>
<h3 id="3._ひとつのアウトラインで、俯瞰から詳細まで（視点に応じた階層折りたたみ）">3. ひとつのアウトラインで、俯瞰から詳細まで（視点に応じた階層折りたたみ）</h3>
<p>親タスクを持つタスクは自動的に <strong>サマリー（まとめタスク）</strong> となり、配下のタスクの期間や進捗率を自動で集計します。</p>
<ul>
<li><code>zm</code> / <code>zr</code> で階層レベルを折りたたみ・展開。</li>
<li><code>zM</code> で全プロジェクトのサマリーのみを 1 画面に集約して表示（マネージャー視点の全体俯瞰）。</li>
<li><code>zR</code> で最も深い実装タスクまで一括展開（作業者視点での詳細確認）。</li>
<li><code>zf</code> で選択したサブツリーだけにズームイン、<code>zF</code> で戻る。</li>
</ul>
<p>同じデータ構造のまま、表示する階層の深さ（視点）を切り替えるだけで、全体像の把握から具体的な作業まで柔軟に対応できます。</p>
<h3 id="4._サーバー不要の_P2P_同期_(iroh)">4. サーバー不要の P2P 同期 (iroh)</h3>
<p><a rel="external" href="https://iroh.computer">iroh</a> を採用しており、中央サーバーを必要としない直接 P2P 同期を実現しています。</p>
<ul>
<li>各レプリカ（環境）が独立して動作し、編集内容は CRDT（Hybrid Logical Clock を用いた Last-Writer-Wins）により競合なく自動マージされます。</li>
<li>パブリックキーと UDP ホールパンチングにより、ポート開放やファイアウォール設定なしで相互接続。</li>
</ul>
<p><strong>同期の手順（ユーザー A がチケットを発行し、ユーザー B が参加する例）：</strong></p>
<pre><code data-lang="sh"># 1. ユーザー A (共有元): アプリ内コマンド `:ticket` や起動ログからチケットを取得して B に共有
# チケット例: yaibaticket1abcdef1234567890abcdef1234567890...

# 2. ユーザー B (参加者): A から共有されたチケットを指定して参加（別プロジェクトとして独立保持）
yaiba join yaibaticket1abcdef1234567890... --as project-a

# (アプリを起動したまま UI から `:join yaibaticket1...` でも参加可能)
</code></pre>
<h3 id="5._画面モードと美しい_UI（Neon_/_Office_/_Super_Mode）">5. 画面モードと美しい UI（Neon / Office / Super Mode）</h3>
<p>利用シーンに合わせて 3 種類のテーマモードを切り替えられます。</p>
<ul>
<li>
<p><strong>Neon Mode</strong>（デフォルト）：サイバーパンク風のネオン HUD。クリティカルパスがマゼンタ色で流れるように強調されます。</p>
</li>
<li>
<p><strong>Office Mode</strong> (<code>gt</code> または <code>:theme light</code>)：ミーティングや画面共有、印刷向けの静かなデザイン。余計な発光や走査線をカットし、実用的な配色になります。</p>
</li>
<li>
<p><strong>Super Mode</strong> (<code>gs</code> または <code>:super</code>)：<strong>中二病全開・男のロマンを極限まで詰め込んだ覚醒モード</strong>。ネオンの発光倍率が解放され、背景に漆黒のオーロラが揺らめき、CRT走査線が走る超絶演出モードです。</p>
<ul>
<li><strong>タイピング即・斬撃</strong>：キーを1文字叩くたびにカーソルから閃光の如く斬撃エフェクトが放たれ、タイピングの連打に合わせて打撃音と反動（リコイル）が増幅！</li>
<li><strong>日本語変換は重打</strong>：日本語の変換確定（Enter）は、単なる1打鍵を超えた深みのある重厚な一撃として画面に刻み込まれます。</li>
<li><strong>タスク完了は衝撃波（ショックウェーブ）</strong>：<code>Space</code> でタスクを完了した瞬間、画面全体に強烈な波紋と衝撃波が駆け巡る！</li>
<li><strong>タスク削除は画面激震</strong>：<code>dd</code> で不要なタスクを滅ぼすと、画面全体がグラリと激しく振る動く！</li>
</ul>
<p>※どんなにエフェクトを限界突破させても、「クリティカルパス＝マゼンタ色」のルールと視覚アクセシビリティ（<code>prefers-reduced-motion</code> 設定時の自動モーションオフ）は厳格に守られる硬派な仕様です。</p>
</li>
<li>
<p><strong>日本語 UI 対応</strong> (<code>:lang ja</code>)：UI全体（ヘルプ、ステータスライン、コマンド拒否メッセージ、列ヘッダー等）を日本語化できます。キー名やコマンド名は統一感を保つため英語のままとなっています。</p>
</li>
</ul>
<h3 id="6._マウス操作と右クリックメニュー">6. マウス操作と右クリックメニュー</h3>
<p>キーボード操作が基本ですが、画面共有時や他人に操作を譲る場合のためにマウス操作もサポートしています。</p>
<ul>
<li>タスクやバーのドラッグ＆ドロップ（開始日・期間の調整、並び替え、依存関係の接続）。</li>
<li>右クリックメニュー：キーボード操作のショートカットキー（<code>dd</code>, <code>s</code>, <code>gp</code>, <code>zf</code> など）が併記されており、<strong>「マウス操作をしながらキーボードショートカットを覚えられる」</strong> ガイドの役割を果たします。</li>
</ul>
<h3 id="7._計画_vs_実績_(Plan_vs_Actual)_と_イナズマ線_(Progress_Line)">7. 計画 vs 実績 (Plan vs Actual) と イナズマ線 (Progress Line)</h3>
<ul>
<li>計画日付（<code>start</code>, <code>duration</code>）と実績日付（<code>began</code>, <code>ended</code>）を両方記録。</li>
<li><code>gd</code> で計画と実績の列表示（Date Columns）とコンパクト表示をトグル。</li>
<li>基準日（As-of date）を設定可能（<code>:asof -3d</code> や top バーの日付クリック）。</li>
<li>ガントチャート上に <strong>イナズマ線</strong> を描画。基準日に対して各タスクが予定通りか、遅れているか（左への折れ）、進んでいるか（右への膨らみ）が視覚的にひと目で把握できます。</li>
</ul>
<h3 id="8._複数プロジェクト管理_(projects.toml)">8. 複数プロジェクト管理 (<code>projects.toml</code>)</h3>
<p>ひとつの <code>yaiba</code> プロセスで複数のプロジェクト（データベースファイル）を登録・切り替え可能です。</p>
<ul>
<li><code>:proj</code> またはトップバーのプロジェクト名クリックで曖昧検索（Fuzzy Picker）つきのプロジェクト切り替えダイアログを起動。</li>
<li>P2P で参加した相手のタスク群は別プロジェクトとして孤立して保持されるため、自分のバックログと混ざる心配がありません。</li>
<li>完全に融合させたい場合は <code>yaiba merge &lt;ticket&gt;</code> を使用します。</li>
</ul>
<h3 id="9._AI_エージェント連携_(yaiba_mcp)">9. AI エージェント連携 (<code>yaiba mcp</code>)</h3>
<p>Model Context Protocol (MCP) に対応しており、Claude Code や Cursor などの AI エージェントから計画の参照・更新が可能です。</p>
<pre><code data-lang="sh"># バックグラウンドで起動中の yaiba に対して MCP サーバーを登録
claude mcp add yaiba -- yaiba mcp
</code></pre>
<p>エージェントは <code>plan</code> ツールで依存関係やクリティカルパス・遅延状況を把握し、<code>add_task</code> や <code>link</code> ツールでタスクの分解やリンク設定を自律的に行えます。</p>
<p>例えば、GitHub の Issue 群やロードマップのテキストを渡して <strong>「この内容を元に、タスクを起こしてスケジュールを組んで」</strong> と頼むだけで、適切な親子の階層構造や依存関係（クリティカルパス含む）が設定されたガントチャートがあっという間に組み上がります。</p>
<p><img src="/static/images/2026-08-08_yaiba_mcp_gantt.png" alt="yaiba MCP で自動生成されたガントチャートの例" /></p>
<p>手動でタスクを1つずつ打ち込んで繋ぐ手間すらなくなり、AI と対話するだけでプロジェクト計画の初動が完了します。</p>
<hr />
<h2 id="インストールと使い方">インストールと使い方</h2>
<h3 id="インストール">インストール</h3>
<p>crates.io からインストールするか、<a rel="external" href="https://github.com/yukimemi/yaiba/releases">GitHub Releases</a> からビルド済みバイナリ（Linux / macOS / Windows）をダウンロードします。</p>
<pre><code data-lang="sh">cargo install yaiba
</code></pre>
<h3 id="クイックスタート">クイックスタート</h3>
<p>ターミナルで <code>yaiba</code> を実行すると、ローカルサーバーが起動し、自動的にブラウザで <code>http://localhost:8188</code> が開きます。</p>
<pre><code data-lang="sh"># 起動（デフォルトでブラウザが開きます）
yaiba

# ポート変更やブラウザ起動なし
yaiba --port 9000 --no-open

# ネットワーク非接続（完全ローカルモード）
yaiba --no-sync
</code></pre>
<h3 id="主なキーバインド・コマンド">主なキーバインド・コマンド</h3>
<table><thead><tr><th>キー / コマンド</th><th>説明</th></tr></thead><tbody>
<tr><td><code>?</code></td><td>ヘルプパネルの表示</td></tr>
<tr><td><code>o</code> / <code>O</code></td><td>下 / 上 に新しいタスクを作成</td></tr>
<tr><td><code>space</code> / <code>s</code></td><td>タスクの完了 / ステータス切り替え (<code>todo</code> → <code>doing</code> → <code>done</code>)</td></tr>
<tr><td><code>dd</code> / <code>u</code> / <code>^r</code></td><td>タスクの削除 / Undo / Redo</td></tr>
<tr><td><code>+</code> / <code>-</code></td><td>所要日数を +1日 / -1日</td></tr>
<tr><td><code>.</code> / <code>,</code></td><td>開始日を +1日 / -1日</td></tr>
<tr><td><code>D</code> / <code>X</code></td><td>依存関係の追加 / 削除</td></tr>
<tr><td><code>zm</code> / <code>zr</code></td><td>階層の折りたたみ / 展開（<code>zM</code> / <code>zR</code> で全折りたたみ / 全展開）</td></tr>
<tr><td><code>zf</code> / <code>zF</code></td><td>選択サブツリーへフォーカス / フォーカス解除</td></tr>
<tr><td><code>gd</code></td><td>計画・実績の日付列表示 ⇄ コンパクト表示</td></tr>
<tr><td><code>gt</code> / <code>gs</code></td><td>Officeモード ⇄ Superモード トグル</td></tr>
<tr><td><code>:lang ja</code></td><td>UI全体の日本語化</td></tr>
<tr><td><code>:proj</code></td><td>プロジェクト切り替えピッカーの起動</td></tr>
<tr><td><code>:ticket</code></td><td>同期用チケットのコピー</td></tr>
</tbody></table>
<hr />
<h2 id="おわりに">おわりに</h2>
<p><code>yaiba</code> を作ったことで、「思考の速度でタスクを打ち込み、そのまま依存関係とタイムラインを組み上げ、必要に応じてP2PでチームやAIと共有する」という理想のタスク管理環境が手に入りました。</p>
<p>Webアプリケーションですがシングルバイナリとしてローカルで完結し、動作も非常に軽快です。打鍵感を極めた <strong>Super Mode</strong> でのタスク消化は癖になる爽快感があります。</p>
<p>「マウス中心のガントチャートツールにストレスを感じている方」「Vim のキーバインドでサクサク工程管理をしたい方」「サーバーなしでシンプルにタスクを同期したい方」は、ぜひ <code>yaiba</code> を試してみてください！</p>
<a href="https://github.com/yukimemi/yaiba" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/yaiba: 刃 — vim-flavoured todo &amp; gantt planner. One local binary, embedded UI, peer-to-peer sync over iroh.</div><div class="link-card-description">刃 — vim-flavoured todo &amp; gantt planner. One local binary, embedded UI, peer-to-peer sync over iroh. - yukimemi/yaiba</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/fbe7e5f61a2bfee6673a90c02f77d470def99122c86527d6dbc1bdec912dcdfc/yukimemi/yaiba')"></div></a>
]]></content:encoded>
      </item>
      <item>
          <title>ライブ設定を直接編集、リポジトリが自動追従する dotfiles マネージャー yui</title>
          <link>https://yukimemi.pages.dev/posts/yui/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/yui/</guid>
          <pubDate>Sat, 13 Jun 2026 12:00:00 GMT</pubDate>
          <description>target 側の設定ファイルを直接編集するだけで、自動的にリポジトリ側も更新される dotfiles 管理ツール yui を作りました。chezmoi などの従来ツールで感じていた「編集して適用する 2 ステップの儀式」や「アプリによる直接書き換えでのドリフト」といった気になる点を、ハードリンク・ジャンクション・シンボリックリンクを活用してどう解決したかを紹介します。</description>
          <content:encoded><![CDATA[<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/yukimemi/yui/main/assets/logo-dark.svg" />
    <img src="https://raw.githubusercontent.com/yukimemi/yui/main/assets/logo.svg" width="560" alt="yui 結 — target-as-truth dotfiles manager" />
  </picture>
</p>
<p align="center">
  <b>結 — ライブ設定を直接編集すれば、ソースリポジトリが自動で更新される</b>
</p>
<p>自作の dotfiles 管理ツール <strong>yui (結)</strong> を Rust で作りました。長年愛用してきた chezmoi から移行し、現在は自分の dotfiles リポジトリをこの yui で管理しています。</p>
<a href="https://github.com/yukimemi/yui" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/yui: Target-as-truth dotfiles manager. Edit your live configs, source repo updates automatically via hardlink/junction/symlink.</div><div class="link-card-description">Target-as-truth dotfiles manager. Edit your live configs, source repo updates automatically via hardlink/junction/symlink. - yukimemi/yui</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/2df53bbc09b6fcf5c9d177c4cc3b3f6df3dde1369d6a2de96ef11a640dc598ef/yukimemi/yui')"></div></a>
<h2 id="なぜ作ったのか">なぜ作ったのか</h2>
<p>dotfiles を長年 chezmoi などのツールで管理していて、どうしても拭いきれなかった 3 つの「気になる点」がありました。</p>
<ol>
<li><strong>「編集して適用する」という 2 ステップの儀式 (edit-source-then-apply tax)</strong>
<ul>
<li>設定ファイル（例えば target 側の <code>~/.config/nvim/init.lua</code> など）をちょっといじりたいだけなのに、「リポジトリ内のソースファイルを編集 → <code>chezmoi apply</code> を実行して target 側に適用する」という 2 ステップの儀式が毎回発生し、テンポが悪かった。</li>
</ul>
</li>
<li><strong>ソースとターゲットのドリフト (Source ↔ Target drift)</strong>
<ul>
<li>アプリケーションが target 側の設定ファイルを直接上書き・編集した際、ソースリポジトリとの間に乖離（ドリフト）が発生し、次の <code>chezmoi diff</code> で初めて気づく。</li>
</ul>
</li>
<li><strong>未追跡の新規ファイル (Untracked new files)</strong>
<ul>
<li>Neovim やその他のツールが管理対象ディレクトリ内に新しくファイルを作成しても、手動で <code>chezmoi add</code> をしない限り、リポジトリ側では未追跡のまま放置されてしまう。</li>
</ul>
</li>
</ol>
<p>これらを解決するために、「<strong>対象 (target/ホームディレクトリ) を Truth (真実) にする</strong>」という逆転の発想で設計したのが <strong>yui</strong> です。</p>
<h2 id="yui_のアプローチ:_Target_as_Truth">yui のアプローチ: Target as Truth</h2>
<p>yui は、chezmoi の流れを反転させるだけではありません。
<code>yui apply</code> を一度実行して、ソースリポジトリ（source）と target 側のファイルを物理的なリンクで接続してしまえば、<strong>それ以降は設定を変更するたびに <code>yui apply</code> を走らせる必要がなくなります。</strong></p>
<p>物理リンクによって実体（inode）を共有しているため、エディタ等による target 側への書き込みが、そのまま自動的かつ即時に source 側の書き込みとして反映されます。</p>
<table><thead><tr><th>プラットフォーム</th><th>ファイル</th><th>ディレクトリ</th></tr></thead><tbody>
<tr><td>Linux / macOS</td><td>シンボリックリンク (symlink)</td><td>シンボリックリンク (symlink)</td></tr>
<tr><td>Windows (デフォルト)</td><td><strong>ハードリンク (hardlink)</strong></td><td><strong>ジャンクション (junction)</strong></td></tr>
<tr><td>Windows (オプトイン)</td><td>シンボリックリンク (symlink)</td><td>シンボリックリンク (symlink) (開発者モード/管理者権限が必要)</td></tr>
</tbody></table>
<p>Windows でのデフォルト設定は意図的なものです。ハードリンクとジャンクションはどちらも管理者権限なしで作成可能で、多くのエディタの「アトミック保存」時にも壊れにくいためです。</p>
<p>このアプローチにより、<strong>エディタやアプリから target 側の設定ファイルへ書き込むだけで、リポジトリ側も自動で更新されます</strong>。もう「編集した後に apply を実行する」という手間はありません。</p>
<h3 id="リンク接続のイメージ（ファイルツリー）">リンク接続のイメージ（ファイルツリー）</h3>
<p>基本の配置ルールはリポジトリ直下の <code>config.toml</code> で定義します（例：<code>home/</code> 配下を <code>~</code> にマウント）。<code>yui apply</code> を実行すると、その配下のファイルやフォルダが自動的にリンクされます。</p>
<p>OS別に異なるリンク先を設定したい場合や、デフォルトの配置ルールとは異なる特別な場所にリンクしたい場合は、<code>[[link]]</code> エントリを宣言します。宣言する場所は「対象ディレクトリ内の <code>.yuilink</code> マーカー」と「<code>config.toml</code> の中央 <code>[[link]]</code> テーブル」の 2 択で、どちらも書式は同じです（詳細は後述）。</p>
<pre><code>[Source リポジトリ側] (~/.dotfiles/)
├── config.toml                     # マウント定義 (例: home/ -&gt; ~/) とフック設定
├── home/                           # 基本的にこの配下が自動でリンクされる
│   ├── .gitconfig.tera             # テンプレートファイル
│   ├── .gitconfig                  # レンダリング結果 (gitignored) ━ [デフォルト: Hardlink/Symlink で自動リンク]
│   └── .config/
│       └── nvim/                   # 基本はそのまま ~/.config/nvim/ に自動マッピングされるが...
│           ├── init.lua
│           └── .yuilink            # 🌟Windows用に別の特別なパスを追加で指定するマーカー
└── .yui/
    ├── backup/                     # 自動バックアップ先
    └── state.json                  # マシンごとの状態管理

            │
            ▼ yui apply 実行 (Linux / macOS の場合)
            │

[Dest システム側 (Unix)] (~)
├── .gitconfig                      # (自動接続) home/.gitconfig とリンク共有
└── .config/
    └── nvim/                       # (自動接続) home/.config/nvim/ とリンク共有
        └── init.lua

            │
            ▼ yui apply 実行 (Windows の場合)
            │

[Dest システム側 (Windows)] (~)
├── .gitconfig                      # (自動接続) home/.gitconfig と物理リンクを共有
├── .config/
│   └── nvim/                       # (自動接続) home/.config/nvim/ と物理リンクを共有
│       └── init.lua
└── AppData/Local/nvim/             # 🌟(.yuilink の指定により、こちらにも追加でリンク共有)
    └── init.lua
</code></pre>
<h2 id="アトミック保存対策と_AutoAbsorb_(吸い込み)">アトミック保存対策と AutoAbsorb (吸い込み)</h2>
<p>エディタによっては、ファイルを保存する際に「一時ファイルに書き出してから元のファイル名にリネームする」という<strong>アトミック保存</strong>を行います。これを行うと、せっかく張ったハードリンクやシンボリックリンクが切れてしまいます。</p>
<p>これを解決するために、yui は <code>apply</code> や <code>status</code> を実行した際、<strong>ファイルID (inode や file index) の一致</strong>をチェックします。リンクが切れてしまっている場合、yui の <strong>Absorb Classifier</strong> が以下のように状況を判定します。</p>
<table><thead><tr><th>状態判定</th><th>発生条件</th><th>処理内容</th></tr></thead><tbody>
<tr><td><strong>InSync</strong></td><td>target の file-id == source の file-id</td><td>同期中 (何もしない)</td></tr>
<tr><td><strong>RelinkOnly</strong></td><td>内容は同一だが、file-id が異なる</td><td>再リンクのみ行う</td></tr>
<tr><td><strong>AutoAbsorb</strong></td><td>target の方が更新日時が新しく、内容が異なる</td><td><strong>target（実ファイル）の内容を source（リポジトリ）にコピーし、再リンクする</strong></td></tr>
<tr><td><strong>NeedsConfirm</strong></td><td>source の方が更新日時が新しく、内容が異なる</td><td>競合アノマリーとして確認を求める</td></tr>
<tr><td><strong>Restore</strong></td><td>target が見つからない</td><td>リポジトリから復元する</td></tr>
</tbody></table>
<p>エディタのアトミック保存でハードリンクが切れて内容が変更された場合、それは <code>AutoAbsorb</code> と判定されます。yui は自動的にリポジトリ側のファイルをバックアップした上で、実ファイル（target）の変更内容をリポジトリ（source）に吸い込み、再度リンクを張り直します。</p>
<p>これにより、リンク切れを気にする必要すらなくなります。</p>
<h2 id="特別な配置やOSごとの振り分けを行う_[[link]]_宣言">特別な配置やOSごとの振り分けを行う <code>[[link]]</code> 宣言</h2>
<p>基本的には <code>config.toml</code> に <code>home/</code> を <code>~</code>（ホーム）へマウントする定義を 1 つ書くだけで、配下のディレクトリ（例：<code>home/.config/nvim</code>）も自然にターゲット（<code>~/.config/nvim</code>）へとマッピングされます。</p>
<p>しかし、「<strong>OS別にマッピング先を変えたい</strong>」「<strong>デフォルトの配置ルールから外れる特定の場所へ個別リンクしたい</strong>」「<strong>ディレクトリを丸ごと 1 本のリンクにしたい</strong>」といった場合には、<code>[[link]]</code> エントリを宣言します。書く場所は 2 つあり、スキーマは共通です。</p>
<table><thead><tr><th>書く場所</th><th><code>src</code></th><th>向いているケース</th></tr></thead><tbody>
<tr><td><code>$DOTFILES/config.toml</code>（中央テーブル）</td><td>必須。<code>$DOTFILES</code> からの相対パス</td><td>宣言を 1 ファイルに集約したい。ツリー側にマーカーファイルを増やしたくない</td></tr>
<tr><td><code>&lt;dir&gt;/.yuilink</code>（マーカー）</td><td>省略可。省略すると「このディレクトリ自身」</td><td>宣言がディレクトリと一緒に移動・削除されてほしい</td></tr>
</tbody></table>
<div class="message"><p>中央 <code>[[link]]</code> テーブルは yui <strong>v0.11.0</strong> で追加しました。それ以前は <code>.yuilink</code> マーカーのみです。同じバージョンで、リンクの張り方を決める <code>[link] file_mode / dir_mode</code> は <code>[mount] file_mode / dir_mode</code> へ移動しています（<code>link</code> がリンク宣言の配列になったため）。旧 <code>[link]</code> テーブルが残っていると、移動先を案内するエラーで停止します。</p>
</div>
<h3 id="1つのソースから複数のターゲットへのリンク振り分け">1つのソースから複数のターゲットへのリンク振り分け</h3>
<p>例えば、Neovim の設定を Unix ではデフォルトの <code>~/.config/nvim</code> に配置しつつ、Windows では <code>LOCALAPPDATA/nvim</code> にもマッピングしたいとします。マーカーで書く場合は、<code>home/.config/nvim/.yuilink</code> に以下のように記述します。</p>
<pre><code data-lang="toml"># $DOTFILES/home/.config/nvim/.yuilink
[[link]]
dst = &quot;{{ env(name=&#39;LOCALAPPDATA&#39;) }}/nvim&quot;
when = &quot;yui.os == &#39;windows&#39;&quot;
</code></pre>
<p><code>config.toml</code> に集約する場合は、対象を <code>src</code> で指定します。</p>
<pre><code data-lang="toml"># $DOTFILES/config.toml
[[link]]
src = &quot;home/.config/nvim&quot;
dst = &quot;{{ env(name=&#39;LOCALAPPDATA&#39;) }}/nvim&quot;
when = &quot;yui.os == &#39;windows&#39;&quot;
</code></pre>
<p>これによって、<code>yui apply</code> は環境（OS）の判定を評価し、Windows の場合であれば通常の <code>~/.config/nvim</code> へのマッピングに加え、追加で <code>AppData/Local/nvim</code> にも物理リンクをマッピングしてくれます。</p>
<h3 id="ディレクトリ丸ごとのリンクと、マーカーの「見え方」">ディレクトリ丸ごとのリンクと、マーカーの「見え方」</h3>
<p><code>[[link]]</code> で <strong>ディレクトリを指定</strong>すると、そのディレクトリ自体が 1 本のリンク（Windows ならジャンクション）になります。中のファイルを 1 つずつリンクするわけではないので、アプリがそのディレクトリに新しく作ったファイルもそのまま source 側に現れます。冒頭に挙げた「未追跡の新規ファイル」問題がそもそも発生しない状態です。</p>
<p>加えて、アプリが設定ファイルをアトミック保存（一時ファイルを書いて rename）する場合、ファイル単位のハードリンクは切れて毎回ドリフト扱いになりますが、ディレクトリのジャンクションはこの経路では壊れません。</p>
<p>注意点として、この「ディレクトリ丸ごと」を <code>.yuilink</code> で宣言すると、target 側から見たディレクトリの中にマーカー自身も見えます（例：<code>~/.omp/.yuilink</code>）。アプリの設定ディレクトリにツール固有のファイルを置きたくない場合は、中央テーブルで宣言すると綺麗に収まります。</p>
<h2 id="その他の特徴">その他の特徴</h2>
<ul>
<li>
<p><strong>テンプレート (<code>*.tera</code>) と <code>teravars</code> による柔軟な出し分け</strong></p>
<ul>
<li><a rel="external" href="https://keats.github.io/tera/">Tera</a> テンプレートエンジンをサポートしています。<code>.tera</code> サフィックスの付いたテンプレートファイルはレンダリングされ、出力される実ファイル（<code>.tera</code> サフィックスを落とした実設定ファイル）は<strong>自動的に <code>.gitignore</code> の専用セクション（<code># &gt;&gt;&gt; yui rendered</code> ～ <code>&lt;&lt;&lt;</code> の間）へ追記され、コミット対象から除外されます。</strong> 手動で <code>.gitignore</code> を編集する必要がなく、レンダリング後の中間ファイルを誤ってコミットしてしまう心配がありません。</li>
<li><code>yui</code> は内部で <a rel="external" href="https://github.com/yukimemi/teravars">teravars</a> ライブラリを採用しており、環境変数やシステムコンテキスト（OS、アーキテクチャ等）に加え、<code>config.toml</code> の <code>[vars]</code> テーブルで定義した任意の変数にアクセスできます。</li>
<li>さらに、Git管理から除外される <strong><code>config.local.toml</code></strong> を各マシンに配置し、その中で <code>[vars]</code> の上書き定義（オーバーライド）を行うことで、マシンごとの微調整をシームレスに行えます。</li>
</ul>
<h4 id="具体例：AutoHotkey_の設定切り分け">具体例：AutoHotkey の設定切り分け</h4>
<p>自宅の個人PCと、会社の仕事用PCで AutoHotkey (<code>.ahk</code>) のキー割り当てや起動アプリを切り分けたい場合の実例です。</p>
<p>リポジトリでコミットする <code>config.toml</code> には、共通のデフォルト定義を書いておきます。</p>
<pre><code data-lang="toml"># $DOTFILES/config.toml (共通設定)
[vars.autohotkey]
purpose = &quot;home&quot;
use_new_outlook = true
use_excel = false
use_neovide = true
</code></pre>
<p>仕事用PCには、Git管理外の <code>config.local.toml</code> を作成して仕事用の変数で上書きします。</p>
<pre><code data-lang="toml"># $DOTFILES/config.local.toml (仕事用PCのローカルのみ、gitignored)
[vars.autohotkey]
purpose = &quot;work&quot;
use_excel = true         # 仕事用PCでは Excel ショートカットを有効にする
use_new_outlook = false  # Classic Outlook を使う
</code></pre>
<p>そして、設定ファイル <code>AutoHotkey.ahk.tera</code> では、これらの変数を使って条件分岐を記述します。</p>
<pre><code data-lang="autohotkey">; Outlook (New か Classic か)
^F9::
{
{%- if vars.autohotkey.use_new_outlook %}
  Activate(EnvGet(&quot;LOCALAPPDATA&quot;) . &quot;\Microsoft\WindowsApps\olk.exe&quot;)
{%- elif vars.autohotkey.purpose == &quot;home&quot; %}
  Activate(EnvGet(&quot;LOCALAPPDATA&quot;) . &quot;\Microsoft\WindowsApps\olk.exe&quot;)
{%- else %}
  Activate(&quot;C:\Program Files\Microsoft Office\root\Office16\OUTLOOK.EXE&quot;)
{%- endif %}
}

; Excel (必要な場合のみキー割り当てを有効化)
{%- if vars.autohotkey.use_excel %}
F9::
{
  Activate(&quot;C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE&quot;)
}
{%- endif %}
</code></pre>
<p>このように記述しておくことで、<code>yui apply</code> を叩いた時にそれぞれのマシン固有の設定（自宅PCなら New Outlook の起動、仕事用PCなら Classic Outlook 起動や Excel ホットキー有効化）が施された <code>AutoHotkey.ahk</code> が自動生成されます。</p>
</li>
<li>
<p><strong>秘密情報の暗号化 (<code>*.age</code>)</strong></p>
<ul>
<li><a rel="external" href="https://age-encryption.org/">age</a> による暗号化をサポートしています。Bitwarden や 1Password といった Vault ツールと連携し、新しいマシンで <code>yui secret unlock</code> を実行するだけで秘密鍵を復元できるセキュアな仕組みも用意されています（※セットアップ方法や Vault 連携などの詳細な手順は、<a rel="external" href="https://github.com/yukimemi/yui#secrets-age--opt-in">yui の README (Secrets)</a> をご参照ください）。</li>
</ul>
</li>
<li>
<p><strong>フック (<code>hooks</code>)</strong></p>
<ul>
<li><code>apply</code> の前後に走らせるフックを設定可能。スクリプトファイルの SHA-256 が変化したときだけ走る <code>when_run = "onchange"</code> など、きめ細かい制御ができます。</li>
<li>Deno で書いたスクリプトを走らせることも簡単です。</li>
</ul>
</li>
</ul>
<h2 id="--git-hooks_による安全策"><code>--git-hooks</code> による安全策</h2>
<p><code>yui init --git-hooks</code> を実行すると、リポジトリの <code>.git/hooks/</code> に <code>pre-commit</code> と <code>pre-push</code> のフックがインストールされます。</p>
<p>これらのフックの中身は、いずれも <code>yui render --check</code> を実行するシンプルなものです。</p>
<ul>
<li><strong><code>pre-commit</code></strong>: コミット時に、もしテンプレート（<code>*.tera</code>）からレンダリングされた結果と、実際にステージされているファイルに乖離（ドリフト）がある場合、コミットを拒否します。これにより「適用（<code>yui apply</code> や <code>yui render</code>）を忘れたままコミットしてしまった」というイージーミスを防げます。</li>
<li><strong><code>pre-push</code></strong>: プッシュ時にも同様のチェックを行います。<code>--no-verify</code> などでコミット時のチェックをバイパスした場合のセーフティネットとして機能し、乖離した状態のままリモートにプッシュされるのを防ぎます。</li>
</ul>
<h2 id="インストールとクイックスタート">インストールとクイックスタート</h2>
<h3 id="インストール">インストール</h3>
<pre><code data-lang="sh">cargo install yui-cli
</code></pre>
<h3 id="クイックスタート">クイックスタート</h3>
<pre><code data-lang="sh"># リポジトリの初期化と git hooks の設定
yui init --git-hooks

# $DOTFILES/config.toml を編集してマウントを定義
</code></pre>
<p><code>config.toml</code> の最小構成例：</p>
<pre><code data-lang="toml">[[mount.entry]]
src = &quot;home&quot;
dst = &quot;~&quot;

[[mount.entry]]
src  = &quot;appdata&quot;
dst  = &quot;{{ env(name=&#39;APPDATA&#39;) }}&quot;
when = &quot;yui.os == &#39;windows&#39;&quot;
</code></pre>
<p>あとは以下のコマンドで操作します：</p>
<pre><code data-lang="sh">yui apply   # テンプレート描画 + リンク作成 + ドリフトの自動吸い込み
yui status  # ドリフト状態のチェック
yui list    # マッピング一覧の表示
yui diff    # 乖離しているファイルの差分表示
yui doctor  # 環境チェック
</code></pre>
<h2 id="おわりに">おわりに</h2>
<p>yui を導入したことで、dotfiles 編集の心理的摩擦がほぼゼロになりました。
「あ、Neovim の設定ファイルちょっと書き換えよう」と思った瞬間にいつものエディタで直接開き、保存するだけで、自動的にリポジトリ側のソースが更新されます。あとは必要に応じてリポジトリでコミットするだけです。</p>
<p>「chezmoi の 2ステップの儀式がめんどくさい」「実機設定とリポジトリの同期をシームレスに行いたい」という方は、ぜひ yui を試してみてください。</p>
<a href="https://github.com/yukimemi/yui" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/yui: Target-as-truth dotfiles manager. Edit your live configs, source repo updates automatically via hardlink/junction/symlink.</div><div class="link-card-description">Target-as-truth dotfiles manager. Edit your live configs, source repo updates automatically via hardlink/junction/symlink. - yukimemi/yui</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/2df53bbc09b6fcf5c9d177c4cc3b3f6df3dde1369d6a2de96ef11a640dc598ef/yukimemi/yui')"></div></a>
]]></content:encoded>
      </item>
      <item>
          <title>複数プロジェクトのテンプレを束ねる kata — pj-base を直せば全 PJ に届く</title>
          <link>https://yukimemi.pages.dev/posts/kata/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/kata/</guid>
          <pubDate>Sun, 24 May 2026 12:00:00 GMT</pubDate>
          <description>複数の Rust プロジェクトに同じボイラープレート (Makefile.toml / CI / AGENTS.md など) を適用し続けるためのメタテンプレート CLI、kata を作りました。pj-base への 1 push が全 consumer プロジェクトに伝播していく仕組みと、判断が必要な部分は AI に委譲できる kata の設計を紹介します。</description>
          <content:encoded><![CDATA[<p align="center">
  <img src="https://raw.githubusercontent.com/yukimemi/kata/main/assets/logo.svg" width="560" alt="kata — multi-project template applier with AI-delegated merge" />
</p>
<p><a rel="external" href="https://claude.com/claude-code">Claude Code</a> と一緒に Rust 製の CLI を作るのが楽しくて、ここ最近で <a rel="external" href="https://github.com/yukimemi/shun">shun</a> / <a rel="external" href="https://github.com/yukimemi/rvpm">rvpm</a> / <a rel="external" href="https://github.com/yukimemi/todoke">todoke</a> / <a rel="external" href="https://github.com/yukimemi/yui">yui</a> / <a rel="external" href="https://github.com/yukimemi/renri">renri</a> といった CLI がどんどん増えていきました。これらに<strong>まったく同じボイラープレートを適用しつづける</strong>ためのメタテンプレート CLI、<strong>kata (型)</strong> を作りました。</p>
<blockquote>
<p>型 — <em>the woodblock pattern</em>. 各プロジェクトに同じ型を押し当てる、版木のイメージです。</p>
</blockquote>
<a href="https://github.com/yukimemi/kata" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/kata: Multi-project template applier with AI-delegated merge</div><div class="link-card-description">Multi-project template applier with AI-delegated merge - yukimemi/kata</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/a8fc0db61bdd2b2aa91c9e74f345df25fcc6397dd813f8feabc2556bc0856134/yukimemi/kata')"></div></a>
<h2 id="なぜ作ったのか">なぜ作ったのか</h2>
<p>同じような CLI が増えてくると、共通ボイラープレートのメンテがどんどんしんどくなります。具体的にしんどかったのはこのあたりです。</p>
<ul>
<li><code>Makefile.toml</code> の <code>check</code> / <code>clippy</code> / <code>test</code> タスクの並び</li>
<li><code>.github/workflows/ci.yml</code> の OS マトリクスと action のバージョン pin</li>
<li><code>.github/workflows/release.yml</code> の cross-compile + cargo publish のテンプレ</li>
<li><code>rustfmt.toml</code> / <code>clippy.toml</code> / <code>rust-toolchain.toml</code> の方針</li>
<li><code>apm.yml</code> で <a rel="external" href="https://github.com/microsoft/apm">APM</a> 経由の AI エージェント用 skill (<code>renri</code> など) を入れる定型</li>
<li><code>renovate.json</code> の auto-merge ルール</li>
<li>そして極めつけに <strong><code>AGENTS.md</code> / <code>CLAUDE.md</code> / <code>GEMINI.md</code></strong></li>
</ul>
<p>最後の <code>AGENTS.md</code> がいちばんやっかいでした。Claude / Gemini / Codex に渡している「PR レビューはこう回す」「worktree workflow はこう」「Rust の lint/format ポリシーはこう」みたいな<strong>会話で蓄積されたノウハウ</strong>を、新しいプロジェクトを生やすたびに、あるいは規約を 1 行直すたびに、N 個のリポジトリに<strong>ぜんぶ手でコピペ</strong>する必要があったのです。</p>
<p>「Gemini Code Assist と CodeRabbit 両方のレビュー待つようにしよう」と気付いたら 7 個のリポジトリの <code>AGENTS.md</code> を全部開いて編集する、というのを何度かやりました。1 回ならいいんですけど、規約は<strong>一度決めて終わりじゃなくて</strong>、運用しながら何度も微調整したくなる。</p>
<p>copier / cookiecutter / cruft といった既存ツールは「最初のプロジェクト生成」と「機械的な再適用」までは面倒を見てくれるんですが、<code>AGENTS.md</code> のように</p>
<ul>
<li>リポジトリ共通のセクション (PR レビュー規約、worktree workflow)</li>
<li>プロジェクト固有のセクション (このプロジェクトのアーキ概要、特殊な事情)</li>
</ul>
<p>が<strong>1 枚のファイルに同居している</strong>ようなものを update し続ける機能はありませんでした。AI に判定を委譲するモードもありません。</p>
<p>そこで作ったのが kata です。<strong>「型 (テンプレート) を版木のように押し当てて、押し当てきれないところだけ AI に判断させる」</strong> という発想で設計しました。</p>
<h2 id="kata_の特徴">kata の特徴</h2>
<ul>
<li><strong>layered template</strong> — <code>pj-base</code> + <code>pj-rust</code> + <code>pj-rust-cli</code> を順に重ねて、後勝ち。言語非依存な部分を <code>pj-base</code> に集約できる</li>
<li><strong><code>how</code> × <code>when</code> の二軸</strong> — <code>how</code> (<code>overwrite</code> / <code>merge-section</code> / <code>merge-toml</code> / <code>merge-yaml</code> / <code>ai</code> / <code>script</code>) と <code>when</code> (<code>once</code> / <code>always</code> / <code>manual</code>) が独立。<code>how="ai", when="once"</code> と <code>how="script", when="always"</code> がどちらも自然に書ける</li>
<li><strong>marker-bracketed merge-section</strong> — <code>AGENTS.md</code> の <code>&lt;!-- kata:agents:base:begin --&gt;</code> ～ <code>&lt;!-- kata:agents:base:end --&gt;</code> の<strong>間だけ</strong>を kata が管理。プロジェクト固有の節は外側にいくらでも書ける</li>
<li><strong>path-based merge-toml</strong> — <code>Makefile.toml</code> の <code>tasks.check</code> / <code>tasks.clippy</code> / <code>tasks.test</code> だけを kata が所有して、<code>tasks.install-local</code> のような独自タスクは触らない</li>
<li><strong>AI 委譲</strong> — <code>how = "ai"</code> なファイルは、インストール済みの <code>claude</code> / <code>gemini</code> / <code>codex</code> CLI に template の diff と現在の中身を投げて、chezmoi 風の <code>[a]ccept / [e]dit / [s]kip / [d]efer</code> で確認</li>
<li><strong>truth は PJ 側</strong> — どのテンプレートをどの rev で適用したかは各 PJ の <code>.kata/applied.toml</code> に記録される。グローバル設定は単なる PJ パスのレジストリ</li>
<li><strong>並列実行</strong> — tokio で PJ をファンアウト、AI 呼び出しは semaphore で抑制してエージェント CLI の同時起動が爆発しないようにする</li>
<li><strong>CI 同期</strong> — <code>kata-apply.yml</code> を pj-base が配布。daily で <code>kata update + kata apply</code> を回して、テンプレ上流の変更を PR として自動取り込み</li>
</ul>
<h2 id="インストール">インストール</h2>
<pre><code data-lang="sh">cargo install kata
</code></pre>
<p><code>kata --version</code> で動作確認できれば OK です。</p>
<h2 id="クイックスタート">クイックスタート</h2>
<pre><code data-lang="sh"># 新しい Rust CLI プロジェクトに rust-cli プリセットを適用
mkdir my-rust-cli &amp;&amp; cd my-rust-cli
kata init github.com/yukimemi/pj-presets:rust-cli --non-interactive

# テンプレが進化したら再適用 (idempotent)
kata apply --non-interactive

# 適用前のドライラン
kata status

# 何が tracked か確認
kata list
</code></pre>
<p>これで <code>Makefile.toml</code> / <code>apm.yml</code> / <code>renri.toml</code> / <code>.github/workflows/ci.yml</code> / <code>release.yml</code> / <code>AGENTS.md</code> / <code>CLAUDE.md</code> / <code>GEMINI.md</code> / <code>rustfmt.toml</code> / <code>clippy.toml</code> / <code>rust-toolchain.toml</code> / <code>renovate.json</code> などが<strong>まとめて</strong>プロジェクトに落ちてきます。</p>
<h2 id="preset_=_テンプレートの束">preset = テンプレートの束</h2>
<p><code>pj-presets:rust-cli</code> は単に「どのテンプレートを、どの順に重ねるか」を書いた小さなファイルです。</p>
<pre><code data-lang="toml"># pj-presets/rust-cli.toml
name = &quot;rust-cli&quot;

[[templates]]
source = &quot;github.com/yukimemi/pj-base&quot;

[[templates]]
source = &quot;github.com/yukimemi/pj-rust&quot;

[[templates]]
source = &quot;github.com/yukimemi/pj-rust-cli&quot;
</code></pre>
<p>順に書かれた順番で適用され、同じファイルが衝突したら<strong>後勝ち</strong>です。これによって</p>
<ul>
<li><code>pj-base</code> — 言語非依存 (LICENSE, <code>.gitignore</code>, <code>AGENTS.md</code> の共通節, <code>apm.yml</code>, <code>renri.toml</code> の base, <code>kata-apply.yml</code> ……)</li>
<li><code>pj-rust</code> — Rust 共通 (<code>Makefile.toml</code>, CI matrix, <code>rust-toolchain.toml</code>, <code>rustfmt.toml</code>, <code>clippy.toml</code>)</li>
<li><code>pj-rust-cli</code> — CLI 用追加 (<code>release.yml</code> の cross-compile + cargo publish)</li>
</ul>
<p>という<strong>役割分担</strong>ができて、たとえば「Rust ライブラリだから CLI 用 release は要らない」というケースには <code>pj-rust-lib</code> を組み合わせた別 preset を用意するだけで済みます。</p>
<p>ライブラリ向けに <code>rust-lib</code>、Web フロント向けに <code>web-react</code>、Firebase まで含む <code>web-react-firebase</code> も用意していて、preset 単位で気軽に「型」を切り替えられます。</p>
<h2 id="how_と_when_を独立に持つということ"><code>how</code> と <code>when</code> を独立に持つということ</h2>
<p>kata の設計でいちばんこだわったのが <code>how</code> (適用方法) と <code>when</code> (タイミング) を<strong>別の軸</strong>として持つことです。</p>
<table><thead><tr><th><code>how</code></th><th>何をするか</th></tr></thead><tbody>
<tr><td><code>overwrite</code></td><td>テンプレ通りにファイルを上書き</td></tr>
<tr><td><code>merge-section</code></td><td><code>&lt;!-- kata:*:begin --&gt;</code> ～ <code>end</code> の間だけ差し替え</td></tr>
<tr><td><code>merge-toml</code></td><td><code>toml_edit</code> で指定パスだけマージ</td></tr>
<tr><td><code>merge-yaml</code></td><td><code>serde_yaml</code> で YAML の指定パスだけマージ</td></tr>
<tr><td><code>ai</code></td><td>claude / gemini / codex に判断委譲</td></tr>
<tr><td><code>script</code></td><td>任意のシェルコマンドを実行</td></tr>
</tbody></table>
<table><thead><tr><th><code>when</code></th><th>いつ適用するか</th></tr></thead><tbody>
<tr><td><code>once</code></td><td>初回だけ。以降は <code>.kata/applied.toml</code> の <code>once_applied = true</code> で skip</td></tr>
<tr><td><code>always</code></td><td>毎回適用 (<code>kata apply</code> のたびに同期)</td></tr>
<tr><td><code>manual</code></td><td>明示的に <code>--file</code> を指定したときだけ</td></tr>
</tbody></table>
<p>この 2 軸が独立なので、たとえば</p>
<ul>
<li><code>release.yml</code> は <code>overwrite, when=once</code> — 初回だけ配って、以降はプロジェクト側で自由に編集</li>
<li><code>ci.yml</code> は <code>overwrite, when=always</code> — CI は常に上流追従</li>
<li><code>Makefile.toml</code> は <code>merge-toml, when=always</code> — kata 所有のタスクだけ追従</li>
<li><code>AGENTS.md</code> は <code>merge-section, when=always</code> — マーカーの中だけ追従</li>
<li><code>apm.yml</code> は <code>overwrite, when=once</code> — 初回テンプレ、以降はプロジェクト側</li>
<li><code>LICENSE</code> は <code>overwrite, when=once</code> — 初回だけ</li>
</ul>
<p>という細かいポリシーを 1 つの manifest で表現できます。「mode」として 1 つの enum にまとめなかったのは、<code>how="ai", when="once"</code> (ROADMAP.md の初期生成だけ AI に任せる) のような組み合わせを潰したくなかったからです。</p>
<h2 id="AGENTS.md_の_merge-section_が刺さるところ">AGENTS.md の merge-section が刺さるところ</h2>
<p>kata を入れて一番恩恵を感じているのが <code>AGENTS.md</code> の扱いなので、ここは少し詳しく書きます。</p>
<p><code>pj-base</code> 側の <code>AGENTS.md.base</code> (テンプレ) には共通規約だけが書かれています。</p>
<pre><code data-lang="markdown">## Shared conventions

This file is the agent-agnostic source of truth (per the
[agents.md](https://agents.md) convention)...

### Git workflow
- **No direct push to `main`.** Open a PR.
- Branch names: `feat/...`, `fix/...`, `chore/...`.
- **PR titles + bodies in English.**
...

### PR review cycle
- Every PR runs reviews from **Gemini Code Assist** and **CodeRabbit**...
- **After opening a PR, immediately enter the review-monitoring loop...**
...

### Worktree workflow
Use [`renri`](https://github.com/yukimemi/renri) for any commit-bound change...
</code></pre>
<p>これを <code>pj-base/template.toml</code> でこう宣言しています。</p>
<pre><code data-lang="toml">[[file]]
src = &quot;AGENTS.md.base&quot;
dst = &quot;AGENTS.md&quot;
how = &quot;merge-section&quot;
when = &quot;always&quot;
marker = { begin = &quot;&lt;!-- kata:agents:base:begin --&gt;&quot;, end = &quot;&lt;!-- kata:agents:base:end --&gt;&quot; }
</code></pre>
<p><code>kata apply</code> を実行すると、各プロジェクトの <code>AGENTS.md</code> の対応マーカーの<strong>中だけ</strong>がテンプレ内容で置き換わります。マーカーの外には<strong>そのプロジェクトの固有の事情</strong> (アーキテクチャ、設計判断、ドメイン用語、Phase n の状況……) をいくらでも書いておけて、それは <code>kata apply</code> で一切触られません。</p>
<p>さらに layered なので、<code>pj-rust</code> は <code>&lt;!-- kata:agents:rust:* --&gt;</code>、<code>pj-rust-cli</code> は <code>&lt;!-- kata:agents:rust-cli:* --&gt;</code> と<strong>それぞれ自分のマーカーブロックを所有</strong>します。これによって 1 枚の <code>AGENTS.md</code> の中に「言語非依存 / Rust 共通 / Rust CLI 専用 / プロジェクト固有」の 4 層が綺麗に同居する、という構造になります。</p>
<h2 id="Makefile.toml_の_merge-toml_で「kata_所有タスク」だけ追従させる">Makefile.toml の merge-toml で「kata 所有タスク」だけ追従させる</h2>
<p><code>AGENTS.md</code> の marker と並んでよく使うのが、<code>Makefile.toml</code> の <code>merge-toml</code> です。<code>pj-rust/template.toml</code> ではこう宣言しています。</p>
<pre><code data-lang="toml">[[file]]
src = &quot;Makefile.toml&quot;
how = &quot;merge-toml&quot;
when = &quot;always&quot;
# kata owns these specific tasks; everything else is left untouched
paths = [
    &quot;tasks.default&quot;,
    &quot;tasks.check&quot;,
    &quot;tasks.fmt&quot;,
    &quot;tasks.fmt-check&quot;,
    &quot;tasks.clippy&quot;,
    &quot;tasks.test&quot;,
    &quot;tasks.lock-check&quot;,
    &quot;tasks.publish-dry&quot;,
    &quot;tasks.hook-install&quot;,
    &quot;tasks.apm-install&quot;,
    &quot;tasks.setup&quot;,
    &quot;tasks.on-add&quot;,
]
</code></pre>
<p><code>paths</code> で「<strong>kata が所有する TOML のパス</strong>」を明示します。<code>tasks.check</code> / <code>tasks.clippy</code> / <code>tasks.test</code> のような kata-managed task は毎回上流から上書きされますが、consumer 側で勝手に追加した <code>tasks.install-local</code> とか <code>tasks.deploy</code> のような独自タスクは <strong>paths に含まれないので一切触られない</strong> という挙動になります。</p>
<p><code>merge-toml</code> は <code>toml_edit</code> で <code>paths</code> で指定した key だけを replace していくので、</p>
<ul>
<li>同じ key の値は更新される (例: <code>tasks.check.script</code> の中身が変わる)</li>
<li>paths に出てこない key は consumer の手書きが完全に保持</li>
<li>インデント / コメント / 順序も <code>toml_edit</code> レベルで保持</li>
</ul>
<p>という、<code>overwrite</code> だと潰してしまう手書き内容を尊重した同期ができます。</p>
<h2 id="GHA_の_action_バージョンは_.kata/vars.toml_に切り出して_Renovate_に任せる">GHA の action バージョンは <code>.kata/vars.toml</code> に切り出して Renovate に任せる</h2>
<p><code>merge-toml</code> × <code>when = "once"</code> のもう 1 つの実用例が、<strong>GitHub Actions の version pin を <code>.kata/vars.toml</code> (Tera 変数ファイル) に切り出す</strong> パターンです。</p>
<p>考えたい問題はこうです。</p>
<ul>
<li><code>ci.yml</code> / <code>release.yml</code> / <code>kata-apply.yml</code> の version pin (<code>actions/checkout@v6.0.2</code> 等) を <strong>Renovate に自動 bump</strong> させたい</li>
<li>しかし workflow 本体は <code>overwrite, when=always</code> で kata-managed なので、Renovate が直接 <code>ci.yml</code> を編集しても次の <code>kata apply</code> で潰されてしまう</li>
</ul>
<p>解決策は、<strong>pin だけを <code>.kata/vars.toml</code> に切り出して、workflow 本体は Tera テンプレでそれを参照</strong> することです。</p>
<p><code>pj-base/vars.toml</code> (universal pin):</p>
<pre><code data-lang="toml">[actions]
checkout = &quot;actions/checkout@v6.0.2&quot;
create_pull_request = &quot;peter-evans/create-pull-request@v8.1.1&quot;
</code></pre>
<p><code>pj-rust/vars.rust.toml</code> で Rust 専用の pin を<strong>追加マージ</strong>:</p>
<pre><code data-lang="toml">[actions]
swatinem_rust_cache = &quot;Swatinem/rust-cache@v2&quot;
</code></pre>
<p><code>template.toml</code> 側で <code>.kata/vars.toml</code> の所有関係を宣言:</p>
<pre><code data-lang="toml"># pj-base — universal pin を初回だけ seed
[[file]]
src = &quot;vars.toml&quot;
dst = &quot;.kata/vars.toml&quot;
how = &quot;overwrite&quot;
when = &quot;once&quot;

# pj-rust — Rust 専用 pin を merge-toml で重ねる (初回だけ)
[[file]]
src = &quot;vars.rust.toml&quot;
dst = &quot;.kata/vars.toml&quot;
how = &quot;merge-toml&quot;
when = &quot;once&quot;
paths = [&quot;actions.swatinem_rust_cache&quot;]
</code></pre>
<p>両方とも <code>when = "once"</code> なので、<strong>初回 apply で seed したあとは consumer の <code>.kata/vars.toml</code> を kata は一切触らない</strong>、という所有関係になります。<code>ci.yml.tera</code> の中では:</p>
<pre><code data-lang="yaml">- uses: {{ vars.actions.checkout }}
- uses: {{ vars.actions.swatinem_rust_cache }}
</code></pre>
<p>として参照されているので、<code>.kata/vars.toml</code> の pin が変われば次の <code>kata apply</code> で <code>ci.yml</code> 全体が新しい version で再レンダリングされる、という流れになります。</p>
<p>その上で <strong>consumer 側の <code>.kata/vars.toml</code> を Renovate の customManager が scan</strong> していて、新しい action リリースが出たら <strong><code>.kata/vars.toml</code> の pin 値を bump する PR</strong> を作ってくれます。Renovate が触るのは <code>.kata/vars.toml</code> の 1 行だけ、workflow 本体は kata-apply の再レンダリングで反映、という分業になります。</p>
<p>まとめると、</p>
<ul>
<li><strong>workflow の構造変更</strong> (新ステップ追加、jobs の整理など) → 上流テンプレへの push → daily <code>kata-apply</code> で降りてくる</li>
<li><strong>action の version bump</strong> → consumer の <code>.kata/vars.toml</code> を Renovate が自走で書き換える → 次の <code>kata apply</code> で <code>ci.yml</code> が再レンダリング</li>
</ul>
<p>という、<strong>役割分担の明快な並列同期</strong>が <code>merge-toml</code> と <code>when = "once"</code> の組み合わせだけで組めます。</p>
<p>ところで、上の <code>ci.yml.tera</code> で <code>{{ vars.actions.checkout }}</code> のように書けているのは Rust 製のテンプレートエンジン <a rel="external" href="https://keats.github.io/tera/">Tera</a> のおかげです。<code>.tera</code> サフィックスの付いたファイルが apply 時に Tera で render されて、suffix を落とした名前で consumer に書き出される、というシンプルな仕組み。<code>{% if is_windows() %}</code> のような分岐も <code>{{ env.HOME }}</code> のような環境変数参照も全部 Tera の機能で、kata 側はほぼ何も再発明していません。</p>
<p>実はこの「設定を Tera で書ける」感覚は、<a rel="external" href="https://github.com/yukimemi/rvpm">rvpm</a> や <a rel="external" href="https://github.com/yukimemi/todoke">todoke</a> といった他の Rust 製 CLI でも同じスタックで提供していて、内部では <a rel="external" href="https://github.com/yukimemi/teravars">teravars</a> (Tera + vars + include + system context を統一した薄いラッパー) を共有しています。<code>rvpm</code> で見慣れた <code>{% if is_windows() %}</code> がそのまま kata の template でも通る、というのが地味に効いていて、Rust で「設定が宣言的に書ける小さな CLI」を作るときの定番スタックとして <strong>teravars</strong> はかなりおすすめです。</p>
<h2 id="ここが本題:_pj-base_を直せば全プロジェクトに反映される">ここが本題: pj-base を直せば全プロジェクトに反映される</h2>
<p>これが kata を作って一番うれしかったところです。</p>
<blockquote>
<p>「あ、<code>AGENTS.md</code> のこの一節、ちょっと書き方が悪かったな。Claude が誤解しがちだから直したい」
「<code>PR review cycle</code> の節、CodeRabbit の rate-limit notice の扱いを追記したい」
「<code>Worktree workflow</code> の節に新しい <code>renri prune</code> の説明を入れたい」</p>
</blockquote>
<p>こういう気付きは規約を運用してるとめちゃくちゃ頻繁にあります。kata を入れる前は <strong>N 個の <code>AGENTS.md</code> を全部開いて同じ編集を N 回繰り返す</strong> ことになっていました。</p>
<p>kata を入れた今はこうなっています。</p>
<pre><code data-lang="sh"># pj-base 側でだけ直す
cd ~/src/github.com/yukimemi/pj-base
$EDITOR AGENTS.md.base
git commit -am &quot;docs(agents): clarify PR review cycle&quot;
git push

# あとは何もしなくていい — 翌日になれば、
# kata-apply ワークフローが全 PJ に PR を作って auto-merge する
</code></pre>
<p>具体的には、各プロジェクトの <code>.github/workflows/kata-apply.yml</code> (これも pj-base 配布) が毎日 03:17 UTC に走って、</p>
<ol>
<li>上流テンプレ (<code>pj-base</code> / <code>pj-rust</code> / <code>pj-rust-cli</code>) の最新 rev を取得</li>
<li><code>kata update</code> で applied.toml の rev を更新</li>
<li><code>kata apply --non-interactive --no-ai</code> で再レンダリング</li>
<li>差分が出たら <code>kata-apply/auto</code> ブランチに PR を作成</li>
<li>CI が緑なら <strong>auto-merge</strong></li>
</ol>
<p>を全リポジトリで自動的に回します。<strong>規約や設定の更新が該当するテンプレレイヤへの 1 push だけで N プロジェクトに伝播していく</strong>わけです。<code>AGENTS.md</code> の共通節なら <code>pj-base</code>、<code>Makefile.toml</code> / <code>rustfmt.toml</code> / <code>clippy.toml</code> / <code>ci.yml</code> といった Rust 共通の規約なら <code>pj-rust</code>、<code>release.yml</code> の cross-compile + cargo publish なら <code>pj-rust-cli</code>、というレイヤ分担そのままに、<strong>それぞれの上流に 1 push</strong> すれば全 consumer PJ が翌日には追従します。</p>
<p>ワークフロー本体もテンプレ管理なので、ワークフロー自体の改善 (新しい action バージョン、ステップの追加) も同じ仕組みで自動的に各プロジェクトに降りていきます。テンプレが自分自身のメンテも見るかたち。</p>
<h2 id="CI_で_kata-apply_を回す">CI で kata-apply を回す</h2>
<p>このフローの肝は <strong>CI で <code>kata apply</code> を回せること</strong>です。設計時点でここを最優先に考えていて、</p>
<ul>
<li><code>--non-interactive</code> でプロンプトを完全にスキップできる</li>
<li><code>--no-ai</code> で AI ファイルを丸ごとスキップ (= 機械的なテンプレ同期だけ自動で回せる)</li>
<li><code>--non-interactive --yes</code> で「全部 accept」モードもあり (= 信頼できるテンプレなら AI も自動 accept)</li>
</ul>
<p>という 3 つのフラグで CI 適性が担保されています。</p>
<p><code>pj-base</code> が配布する <code>kata-apply.yml.tera</code> の中身は essentially こんな感じです (一部抜粋)。</p>
<pre><code data-lang="yaml">name: kata-apply

on:
  schedule:
    - cron: &quot;17 3 * * *&quot;     # 03:17 UTC daily
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

concurrency:
  group: kata-apply
  cancel-in-progress: false

jobs:
  apply:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6.0.2
        with:
          token: ${{ secrets.KATA_APPLY_TOKEN }}    # ← PAT 必須

      - name: Install kata
        run: |
          KATA_VERSION=&quot;$(curl -fsSL https://api.github.com/repos/yukimemi/kata/releases/latest | jq -r .tag_name)&quot;
          curl -fsSL &quot;https://github.com/yukimemi/kata/releases/download/${KATA_VERSION}/kata-x86_64-unknown-linux-gnu.tar.gz&quot; \
            | tar xz -C /tmp
          sudo mv /tmp/kata /usr/local/bin/kata

      - name: kata update + apply
        run: |
          kata update
          kata apply --non-interactive --no-ai

      - name: Open / update PR if there are changes
        id: cpr
        uses: peter-evans/create-pull-request@v8.1.1
        with:
          token: ${{ secrets.KATA_APPLY_TOKEN }}
          branch: kata-apply/auto
          title: &quot;chore(kata): auto-apply&quot;

      - name: Enable auto-merge
        if: steps.cpr.outputs.pull-request-number != &#39;&#39;
        env:
          GH_TOKEN: ${{ secrets.KATA_APPLY_TOKEN }}
        run: |
          gh pr merge --auto --squash ${{ steps.cpr.outputs.pull-request-number }}
</code></pre>
<p>要点だけハイライトしておきます。</p>
<ul>
<li><strong><code>KATA_APPLY_TOKEN</code> は <code>GITHUB_TOKEN</code> じゃダメ</strong>。<code>GITHUB_TOKEN</code> 経由で開いた PR は GitHub のループ防止仕様で<strong>下流ワークフロー (CI) を triggers しません</strong>。auto-merge の前提が CI 緑判定なので、CI が走らないと永遠にマージされません。適切な権限 (<code>contents: write</code> + <code>pull-requests: write</code>) を付けた PAT (fine-grained でも classic でも可) を <code>KATA_APPLY_TOKEN</code> リポジトリシークレットとして設定する、というのが consumer 側の唯一のセットアップ作業です</li>
<li><strong>ブランチは <code>kata-apply/auto</code> 固定</strong>。<code>create-pull-request</code> の <code>delete-branch: true</code> と組み合わせると、毎日新しい差分を同じブランチに rolling で上書きしていく動きになるので、PR が大量にスタックしません</li>
<li><strong>CI が落ちたら PR は open のまま残る</strong>。auto-merge は CI 緑になったときだけ発火するので、何か壊れたら人間が見るタイミングが自然に生まれます</li>
<li><strong><code>cron: "17 3 * * *"</code> は意図的な off-peak かつ off-the-hour pin</strong>。<code>0 0 * * *</code> や <code>0 9 * * *</code> みたいに :00 ちょうどでスケジュールする人が地球上に多すぎて、GitHub Actions の runner プールが :00 / :30 で枯渇する (cron-storm) という現象があります。<code>:17</code> のような半端な分にずらすと runner 確保がスムーズ。さらに <code>3 UTC</code> は日本時間 12:00 / 米西海岸の夜 / 欧州早朝で、runner 自体も比較的空いている時間帯なので、daily な機械的同期にはちょうどいい枠です</li>
</ul>
<p>「全プロジェクトに同じワークフローが入っている」という事実が、<strong>全プロジェクトに同じ自動同期がかかっている</strong> という安心感に繋がっていて、これは入れた価値が大きかったです。</p>
<h2 id="AI_委譲モード">AI 委譲モード</h2>
<p><code>how = "ai"</code> を指定したファイルは、インストール済みの AI CLI に判断を投げます。</p>
<p>template.toml にこう書くと:</p>
<pre><code data-lang="toml">[[file]]
src = &quot;ROADMAP.md.tera&quot;
dst = &quot;ROADMAP.md&quot;
how = &quot;ai&quot;
when = &quot;always&quot;
agent = &quot;auto&quot;                # claude &gt; codex &gt; gemini で最初に見つかったやつ
prompt = &quot;&quot;&quot;
Merge the template&#39;s structural changes into the project&#39;s
ROADMAP.md. Preserve project-specific phases and dated entries.
&quot;&quot;&quot;
</code></pre>
<p>kata は template の diff、現在の dst の中身、prompt をまとめて指定エージェントの CLI (<code>claude -p</code>, <code>gemini -p</code>, <code>codex exec</code> のいずれか) に投げます。返ってきた full body または patch に対して、chezmoi 風の対話プロンプトが出ます。</p>
<pre><code>proposed change for ROADMAP.md:
  + Phase 5 — opencode adapter
  + ## Crate structure (regenerated section)
  ...

[a]ccept / [e]dit / [s]kip / [d]efer ?
</code></pre>
<ul>
<li><code>a</code> = そのまま採用</li>
<li><code>e</code> = <code>$EDITOR</code> で開いて手で直してから採用</li>
<li><code>s</code> = この回はスキップ (次回 <code>kata apply</code> でもう一度提案される)</li>
<li><code>d</code> = <code>defer</code> (今回は見送り、ただし「次回必ず再提案」を <code>applied.toml</code> に記録)</li>
</ul>
<p><code>--non-interactive</code> だけだと安全側に倒れて AI ファイルはスキップ、<code>--non-interactive --yes</code> だと<strong>全部 accept</strong> という CI 完全自動モードになります。</p>
<p>backend は trait で抽象化されていて、<code>claude</code> / <code>gemini</code> / <code>codex</code> の 3 つを実装済みです。<code>agent = "auto"</code> は PATH を見て上から順にフォールバックしていく挙動。<code>MockAiAgent</code> も組み込まれていて、テストでは決定的な応答が返せます。</p>
<p>並列度の制御もあって、AI 呼び出しはグローバル semaphore (default 4) で絞られます。<code>kata apply --all</code> で 10 個のプロジェクトを並列で回したときに、エージェント CLI の同時起動が爆発しないようにするためです。</p>
<h2 id=".kata/applied.toml_が_source_of_truth"><code>.kata/applied.toml</code> が source of truth</h2>
<p>kata の状態は全部 PJ 側の <code>.kata/applied.toml</code> に書かれます。グローバル設定 (<code>~/.config/kata/config.toml</code>) は単なる PJ パスのレジストリで、<strong>何が適用されているか</strong>は知りません。</p>
<p><code>applied.toml</code> はだいたいこんな感じです。</p>
<pre><code data-lang="toml">preset = &quot;github.com/yukimemi/pj-presets:rust-cli&quot;
applied_at = &quot;2026-05-17T04:40:43Z&quot;

[[templates]]
source = &quot;github.com/yukimemi/pj-base&quot;
rev = &quot;f04151faf4f0678be9621bb724c8f3120a5e4d8b&quot;
version = &quot;0.10.0&quot;

[[templates]]
source = &quot;github.com/yukimemi/pj-rust&quot;
rev = &quot;9f103ca5aaf39e1dcf1a4d84b11685821aabc62f&quot;
version = &quot;0.5.0&quot;

[[templates]]
source = &quot;github.com/yukimemi/pj-rust-cli&quot;
rev = &quot;9263751c3f94f3415147e546db33170157fb1503&quot;
version = &quot;0.2.0&quot;

[files.&quot;AGENTS.md&quot;]
content_hash = &quot;e4f146a1c66d11e6b6c707f52507b3e37da2fe52d8d7cf13f75edcf9ad5d3a7f&quot;

[files.&quot;Makefile.toml&quot;]
content_hash = &quot;27e3f57b9efd6c177843d9b0d248f40af5843739bcde6ceaf985f2ce518ecfcf&quot;

[files.&quot;LICENSE&quot;]
once_applied = true
</code></pre>
<p>これを git に commit しておくと、</p>
<ul>
<li><strong>teammate</strong> が clone → <code>kata apply</code> で同じ状態が再現できる</li>
<li><strong>CI</strong> が <code>applied.toml</code> を見るので、ローカル設定なしで CI が完結する</li>
<li><strong>rev が pin されている</strong> ので、上流の HEAD が動いても勝手に当たらない (<code>kata update</code> で明示的に上げる)</li>
</ul>
<p>という運用になります。「状態は対象 (PJ) 側に置く、グローバル設定は単なるレジストリに留める」という設計を kata でも採用しました。</p>
<h2 id="kata_apply_が冪等であること"><code>kata apply</code> が冪等であること</h2>
<p>設計でずっと気をつけたのが「<strong>何度走らせても結果が変わらない</strong>」ことです。<code>apply</code> が走るたびに差分が出るようなツールは CI で回せないので、</p>
<ul>
<li><code>content_hash</code> を <code>applied.toml</code> に記録して、変化がないファイルはそもそも書き換えない</li>
<li><code>once_applied = true</code> のファイルは 2 回目以降は完全 skip</li>
<li><code>merge-section</code> / <code>merge-toml</code> のマージは<strong>べき等</strong>なように実装 (同じ入力で何度マージしても同じ出力)</li>
<li>AI モードも <code>--non-interactive --no-ai</code> で完全に固定動作 (= AI モードを除いて再現可能)</li>
</ul>
<p>という不変条件を守るようにしてあります。</p>
<p>おかげで <code>kata apply --non-interactive --no-ai</code> を毎日 CI で回すと、<strong>変更が必要なときだけ PR が立ち、なければ何も起きない</strong>という静かな動きになります。</p>
<h2 id="関連プロジェクト">関連プロジェクト</h2>
<p>kata の周りに必要な template repo は以下です。すべて単体で意味があるので、好きな組み合わせで preset を組めます。</p>
<table><thead><tr><th>repo</th><th>役割</th></tr></thead><tbody>
<tr><td><a rel="external" href="https://github.com/yukimemi/pj-base"><code>pj-base</code></a></td><td>言語非依存 (LICENSE, <code>.gitignore</code>, AGENTS.md 共通節, kata-apply ワークフロー, …)</td></tr>
<tr><td><a rel="external" href="https://github.com/yukimemi/pj-rust"><code>pj-rust</code></a></td><td>Rust 共通 (Makefile.toml, CI matrix, rust-toolchain, rustfmt, clippy)</td></tr>
<tr><td><a rel="external" href="https://github.com/yukimemi/pj-rust-cli"><code>pj-rust-cli</code></a></td><td>Rust CLI 用 (release.yml の cross-compile + cargo publish)</td></tr>
<tr><td><a rel="external" href="https://github.com/yukimemi/pj-rust-lib"><code>pj-rust-lib</code></a></td><td>Rust ライブラリ用 (crates.io publish のみ、バイナリなし)</td></tr>
<tr><td><a rel="external" href="https://github.com/yukimemi/pj-pnpm"><code>pj-pnpm</code></a></td><td>pnpm / TypeScript 共通</td></tr>
<tr><td><a rel="external" href="https://github.com/yukimemi/pj-react-web"><code>pj-react-web</code></a></td><td>Vite + React + TS + Tailwind</td></tr>
<tr><td><a rel="external" href="https://github.com/yukimemi/pj-firebase"><code>pj-firebase</code></a></td><td>Firebase Hosting + Firestore</td></tr>
<tr><td><a rel="external" href="https://github.com/yukimemi/pj-presets"><code>pj-presets</code></a></td><td><code>rust-cli</code> / <code>rust-lib</code> / <code>web-react</code> / <code>web-react-firebase</code> のバンドル</td></tr>
</tbody></table>
<p>kata 自身も dogfood で <code>pj-presets:rust-cli</code> を適用しています — README の表が示す通り、<code>Makefile.toml</code> も CI も <code>AGENTS.md</code> も全部 kata-apply 経由で同期されています。</p>
<h2 id="おわりに">おわりに</h2>
<p>kata を入れる前は、<code>AGENTS.md</code> を 1 行書き換えるのに 7 リポジトリの編集が必要でした。今は <strong>pj-base に 1 push</strong> すれば、翌日には全プロジェクトに PR が立って auto-merge されています。Claude / Gemini / Codex に渡しているノウハウが<strong>全プロジェクトで瞬時に揃う</strong>、というのが体験として相当よかったです。</p>
<p>「規約を更新する心理的コストが下がると、規約をもっと細かく洗練させたくなる」というポジティブフィードバックが回り始めていて、Claude Code との PR レビューサイクル運用 (<code>/loop</code> での 60s ポーリング、CodeRabbit の rate-limit notice の扱い、version-bump-only PR の特例……) みたいな細かい知見が、書いた次の日には全 PJ の Claude に届くようになりました。</p>
<p>Rust + Tera + tokio + AI CLI 委譲という <a rel="external" href="https://github.com/yukimemi/shun">shun</a> / <a rel="external" href="https://github.com/yukimemi/rvpm">rvpm</a> / <a rel="external" href="https://github.com/yukimemi/todoke">todoke</a> のときから使い倒している組み合わせに、<code>teravars</code> (Tera engine + vars + include の共通エンジン) を載せた構成で、いつもの定番スタックの延長で書けたのも開発体験として良かったところです。</p>
<p>複数のリポジトリのボイラープレートに疲れている方、特に AGENTS.md などの <strong>AI への指示書</strong> を複数プロジェクトに散らかしてしまっている方は、ぜひ kata を試してみてください — <strong>型を押して、版木を当てて、揃えていきましょう</strong>。</p>
<a href="https://github.com/yukimemi/kata" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/kata: Multi-project template applier with AI-delegated merge</div><div class="link-card-description">Multi-project template applier with AI-delegated merge - yukimemi/kata</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/a8fc0db61bdd2b2aa91c9e74f345df25fcc6397dd813f8feabc2556bc0856134/yukimemi/kata')"></div></a>
]]></content:encoded>
      </item>
      <item>
          <title>ルールベースでファイルや URL を届ける Rust 製ディスパッチャ todoke を作った</title>
          <link>https://yukimemi.pages.dev/posts/todoke/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/todoke/</guid>
          <pubDate>Sat, 25 Apr 2026 12:00:00 GMT</pubDate>
          <description>入力されたファイルパス・URL・任意の文字列を TOML のルールに従って、適切なエディタやスクリプトに「届け」る Rust 製ディスパッチャ todoke を作りました。$EDITOR や OS の既定プログラムとして使うのが想定用途です。</description>
          <content:encoded><![CDATA[<p align="center">
  <img src="https://raw.githubusercontent.com/yukimemi/todoke/main/assets/logo.svg" width="560" alt="todoke — rule-driven file dispatcher" />
</p>
<p>自作のファイル・URL ディスパッチャ <strong>todoke (届け)</strong> を <a rel="external" href="https://claude.com/claude-code">Claude Code</a> と一緒に作りました。Rust 製で、入力された引数（ファイルパス・URL・任意の文字列）を TOML で書いたルールに照らして、対応するエディタ・ブラウザ・スクリプトに引き渡す CLI です。名前のとおり、引数を然るべき相手に**「届け」**ます。</p>
<p>ちなみに、漫画『君に届け』が好きです。</p>
<a href="https://github.com/yukimemi/todoke" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/todoke: A rule-driven file and URL dispatcher: hands incoming paths (or URLs) to the right handler based on TOML-defined rules.</div><div class="link-card-description">A rule-driven file and URL dispatcher: hands incoming paths (or URLs) to the right handler based on TOML-defined rules. - yukimemi/todoke</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/4935e4fad1ce8170e9fce9beb92ccd147b641127aedfbc8d4fb3dbe0e7aefe52/yukimemi/todoke')"></div></a>
<h2 id="なぜ作ったのか">なぜ作ったのか</h2>
<p>きっかけは Neovim を <code>$EDITOR</code> にしたときの、ちょっとした困りごとが積み重なっていたことです。</p>
<ul>
<li><code>git commit</code> のように<strong>呼び出し元をブロックしないと動かない</strong>ものは、新しい nvim を起動して終了を待ちたい</li>
<li>一方、ファイルマネージャからファイルをダブルクリックしたときは、<strong>起動済みの nvim に <code>:edit</code> で送り込みたい</strong></li>
<li>プロジェクトによっては Neovim ではなく <strong>VSCode で開きたい</strong>ものもある</li>
<li>開く URL によって<strong>ブラウザを使い分けたい</strong>（Gmail は Chrome、GitHub は Edge、みたいに）</li>
</ul>
<p>エディタ側の振り分けについては、以前は自作の <a rel="external" href="https://github.com/yukimemi/hitori.vim">hitori.vim</a> を使っていました。<code>$EDITOR=nvim</code> で起動した Neovim 内から「これは別の起動済み nvim に転送すべきか / このまま開くべきか」を判定して、必要なら起動済み nvim にバッファを送り直す、という仕組みです。動作自体はちゃんとしていたのですが、</p>
<ul>
<li>判定のために<strong>いったん Neovim を起動して <a rel="external" href="https://github.com/vim-denops/denops.vim">denops.vim</a> を立ち上げる必要があり</strong>、毎回 Deno のコールドスタートが入って体感が重かった</li>
<li>判定ロジックが Vim プラグイン内に閉じているので、<strong>Neovim 以外のターゲット</strong>（VSCode に流したい / URL はブラウザに渡したい / 独自スキームを処理したい）にスケールしない</li>
<li>Windows のファイル関連付けに置きづらい（Neovim を起動してから判定、という順序がそもそも噛み合わない）</li>
</ul>
<p>要するに、<strong>判定は Neovim の外でやらないと、速度面でも対応範囲の面でも厳しい</strong>、というのが結論でした。</p>
<p>そこで、<strong>「入力を受け取って、ルールに従って正しい相手に届けるだけ」</strong> の単機能ツールを Neovim の外側に切り出すことにしました。それが todoke です。判定が CLI 側で完結するので、<code>$EDITOR</code> 呼び出しはミリ秒で終わるし、Neovim 以外のターゲットにも横展開できます。</p>
<h2 id="todoke_の特徴">todoke の特徴</h2>
<ul>
<li><strong>ルールベースのルーティング</strong> — TOML の正規表現で各入力を届ける。ファイル / URL / 任意文字列のすべてに対応</li>
<li><strong>Neovim インスタンスの再利用</strong> — <code>kind = "neovim"</code> のターゲットは msgpack-RPC で起動中の nvim にぶら下がり、<code>:edit</code> を送る。Windows でも <code>\\.\pipe\...</code> 名前付きパイプで動く</li>
<li><strong>同期 / 非同期をルールごとに指定</strong> — <code>sync = true</code> ならハンドラ終了までブロック（<code>git commit</code> 用）、<code>sync = false</code> なら投げて即終了（OS の関連付け用）</li>
<li><strong>Tera テンプレート対応</strong> — <code>command</code> / <code>listen</code> / <code>args</code> / <code>group</code> などすべての値で <code>{% if is_windows() %}</code> や <code>{{ env.HOME }}</code> が使える</li>
<li><strong>任意の CLI に対応</strong> — <code>code</code> / <code>helix</code> / <code>subl</code> / <code>emacsclient</code> / <code>firefox</code> / <code>bat</code> …いずれもプラグイン不要</li>
<li><strong><code>$EDITOR</code> 互換</strong> — <code>git</code>、<code>crontab</code>、<code>visudo</code>、<code>fc</code>、<code>mutt</code> などの <code>$EDITOR</code> 呼び出し側との互換性を最優先で設計</li>
<li><strong>Windows の OS 既定プログラム</strong> にも設定可能。GUI ターゲットは <code>cmd</code> ウィンドウを点滅させずに起動できる</li>
<li><strong>静的バイナリ</strong> — Rust 製でコールドスタートはミリ秒オーダー</li>
</ul>
<h2 id="インストール">インストール</h2>
<pre><code data-lang="sh">cargo install todoke
</code></pre>
<p><code>~/.cargo/bin/todoke</code> にバイナリが置かれるので、<code>PATH</code> に追加しておきます。</p>
<h2 id="クイックスタート">クイックスタート</h2>
<p>todoke は<strong>設定ファイルがなくても動きます</strong>。バンドルされたデフォルト設定が、<code>COMMIT_EDITMSG</code> などの <code>$EDITOR</code> コールバックは新しい nvim で <code>sync = true</code> 起動、それ以外はすべてひとつの共有 nvim にルーティングする、というルールを最初から持っています。</p>
<p><code>$EDITOR</code> を todoke にしてみるだけでも体験できます。</p>
<pre><code data-lang="sh">export EDITOR=todoke
git commit              # → COMMIT_EDITMSG を nvim mode=new sync=true で開く
todoke notes.md         # → 起動中の nvim に :edit notes.md を送り込む
</code></pre>
<p>カスタマイズしたければ <code>todoke config init</code> で <code>~/.config/todoke/todoke.toml</code> に内蔵 default 設定を書き出してくれるので、そこから編集するのが手っ取り早いです (一度書き出したファイルは todoke 側からは絶対に上書きされません)。<code>todoke config edit</code> なら書き出し + <code>$EDITOR</code> での起動を一発でやってくれます。</p>
<h2 id="設定例">設定例</h2>
<p>ここでは複数のターゲットとルールを組み合わせた、実用的な <code>todoke.toml</code> の例を紹介します。</p>
<pre><code data-lang="toml"># ============================================================
# ターゲット定義 (届ける相手)
# ============================================================

# kind = &quot;neovim&quot; にすると msgpack-RPC で起動中の nvim を再利用する
[todoke.nvim]
kind = &quot;neovim&quot;
command = &quot;nvim&quot;
listen = &#39;{% if is_windows() %}\\.\pipe\nvim-todoke-{{ group }}{% else %}/tmp/nvim-todoke-{{ group }}.sock{% endif %}&#39;

[todoke.code]
command = &quot;code&quot;
[todoke.code.args]
remote = [&quot;--reuse-window&quot;]
new    = [&quot;--new-window&quot;]

# Chrome / Edge を別ターゲットとして登録
[todoke.chrome]
command = &quot;chrome&quot;
gui = true                # Windows: cmd ウィンドウを出さずに起動

[todoke.edge]
command = &quot;msedge&quot;
gui = true

# ============================================================
# ルール (どの入力をどのターゲットに届けるか)
# ============================================================

# git commit / rebase / merge は常に新規 nvim をブロック起動
[[rules]]
name = &quot;editor-callback&quot;
match = &#39;(?i)/(COMMIT_EDITMSG|MERGE_MSG|git-rebase-todo)$&#39;
to = &quot;nvim&quot;
mode = &quot;new&quot;
sync = true

# Gmail は Chrome で開く
[[rules]]
name = &quot;gmail&quot;
match = &#39;^https?://mail\.google\.com/&#39;
to = &quot;chrome&quot;

# GitHub は Edge で開く
[[rules]]
name = &quot;github&quot;
match = &#39;^https?://(www\.)?github\.com/&#39;
to = &quot;edge&quot;

# 仕事のリポジトリだけ VSCode で開く
[[rules]]
name = &quot;work&quot;
match = &#39;/src/company/&#39;
to = &quot;code&quot;
mode = &quot;remote&quot;

# それ以外の URL は Edge にフォールバック
[[rules]]
name = &quot;url-default&quot;
match = &#39;^https?://&#39;
input_type = &quot;url&quot;
to = &quot;edge&quot;

# 残り (主にファイル) は共有 nvim にすべて流す
[[rules]]
name = &quot;default&quot;
match = &#39;.*&#39;
to = &quot;nvim&quot;
group = &quot;default&quot;
mode = &quot;remote&quot;
</code></pre>
<p>これでこういう使い分けができます。</p>
<pre><code data-lang="sh">todoke notes.md                              # → 共有 nvim に :edit
todoke ~/src/company/foo.py                  # → VSCode で開く
todoke https://mail.google.com/mail/u/0/     # → Chrome
todoke https://github.com/yukimemi/todoke    # → Edge
todoke https://example.com                   # → Edge (URL fallback)
git commit                                   # → 新規 nvim (sync) で COMMIT_EDITMSG
</code></pre>
<h2 id="Neovim_の再利用:_kind_=_&quot;neovim&quot;">Neovim の再利用: kind = "neovim"</h2>
<p>todoke の中でいちばん力を入れた機能です。</p>
<p><code>kind = "neovim"</code> のターゲットは、設定された <code>listen</code> パスにある msgpack-RPC ソケット / 名前付きパイプ越しに<strong>起動中の nvim にぶら下がって <code>:edit</code> を送る</strong>だけです。重要なのは、</p>
<ul>
<li>Windows でも <code>\\.\pipe\nvim-todoke-default</code> のような名前付きパイプでそのまま動く</li>
<li>起動済みの nvim がいなければ todoke が <code>nvim --listen &lt;path&gt;</code> で<strong>自動的に起動</strong>する</li>
<li><code>group</code> で nvim インスタンスを論理的に分けられる（<code>group = "work"</code> と <code>group = "private"</code> で別 nvim になる）</li>
</ul>
<p>つまり、<strong>OS のファイルマネージャからダブルクリックしたファイルが、いま開いている nvim のバッファとして開く</strong> という体験が、追加の Vim プラグイン無しで実現します。</p>
<p>さらに <code>group</code> を使えば、ひとつの nvim にすべて流すのではなく、<strong>用途別に複数の nvim インスタンスを使い分ける</strong>こともできます。<code>listen</code> の中に <code>{{ group }}</code> を埋め込んでおけば、group が変わるたびに別の pipe / socket になって、別の nvim プロセスとして起動・再利用される、という仕組みです。</p>
<pre><code data-lang="toml">[todoke.nvim]
kind = &quot;neovim&quot;
command = &quot;nvim&quot;
# listen に {{ group }} を入れておくのが鍵 — group ごとに別 pipe / socket になる
listen = &#39;{% if is_windows() %}\\.\pipe\nvim-todoke-{{ group }}{% else %}/tmp/nvim-todoke-{{ group }}.sock{% endif %}&#39;

# 仕事のリポジトリは &quot;work&quot; グループ
[[rules]]
match = &#39;/src/company/&#39;
to = &quot;nvim&quot;
group = &quot;work&quot;
mode = &quot;remote&quot;

# プライベートは &quot;personal&quot; グループ
[[rules]]
match = &#39;/src/yukimemi/&#39;
to = &quot;nvim&quot;
group = &quot;personal&quot;
mode = &quot;remote&quot;

# その他は共有の &quot;default&quot;
[[rules]]
match = &#39;.*&#39;
to = &quot;nvim&quot;
group = &quot;default&quot;
mode = &quot;remote&quot;
</code></pre>
<p>仕事のファイルは仕事用 nvim、プライベートは別 nvim、それ以外は共有 nvim — どこに届くかは match で決まります。</p>
<h2 id="入力の_3_種類:_file_/_url_/_raw">入力の 3 種類: file / url / raw</h2>
<p>todoke は引数を 3 つの種類に分類します。</p>
<table><thead><tr><th>種類</th><th>例</th><th>マッチ対象の文字列</th></tr></thead><tbody>
<tr><td><code>file</code></td><td><code>notes.md</code>, <code>Makefile</code>, <code>/tmp/new.md</code></td><td>正規化された絶対パス（<code>/</code> 区切り）</td></tr>
<tr><td><code>url</code></td><td><code>https://example.com</code></td><td>URL そのもの</td></tr>
<tr><td><code>raw</code></td><td><code>HEAD</code>, <code>main</code></td><td>引数の生文字列</td></tr>
</tbody></table>
<p>ファイル / URL は形から自動判定されます。ファイルとして存在しない <code>Makefile</code> や <code>newfile.txt</code> も、ファイル<strong>らしい</strong>形であれば file として扱われるので、<code>vim Makefile</code> と同じ感覚で <code>todoke Makefile</code> が動きます。</p>
<p><code>HEAD</code> や <code>main</code> のような曖昧な文字列は、<code>--todoke-as raw</code> で明示するか、ルール側で <code>input_type = "raw"</code> に固定すれば「git ref を GitHub のツリービューで開く」みたいな運用ができます。</p>
<pre><code data-lang="toml">[[rules]]
name = &quot;gh-ref&quot;
match = &#39;^(HEAD|main|master|develop|v?\d+\.\d+\.\d+|[0-9a-f]{7,40})$&#39;
to = &quot;gh-ref&quot;
input_type = &quot;raw&quot;      # ← ローカルの &quot;main&quot; ファイルと衝突しないよう raw に固定
</code></pre>
<h2 id="エディタ系フラグの取り回し:_passthrough">エディタ系フラグの取り回し: passthrough</h2>
<p><code>$EDITOR=todoke</code> でいざ運用してみると、<code>+42 file.txt</code> のような vim スタイルのフラグを渡してくる呼び出し元が出てきます。todoke は何もしないと <code>+42</code> を「ファイルパス」と誤認識してしまうので、<strong>「これはフラグなのでファイルや URL のような入力としては扱わず、引数としてターゲットに forward する」</strong> と書く仕組みが必要になります。それが <code>passthrough</code> です。</p>
<pre><code data-lang="toml"># どのフラグもターゲット非依存で素通しする
[[rules]]
name = &quot;any-flag&quot;
match = &#39;^[-+]&#39;
passthrough = true        # to は不要 — 同じ batch で他のルールが選んだターゲットに合流する

[[rules]]
name = &quot;nvim-file&quot;
match = &#39;.*&#39;
to = &quot;nvim-term&quot;
sync = true
</code></pre>
<p>これで <code>todoke +42 foo.txt</code> は <code>nvim +42 foo.txt</code> として起動します。<code>-c :set ft=md</code> のような <strong>値が次の argv にあるフラグ</strong>には <code>consumes</code>、<code>-p a.txt b.txt c.txt</code> のような <strong>可変長</strong> には <code>consumes_until</code>、<code>--</code> で分けるタイプには <code>consumes_rest</code> を使います。</p>
<pre><code data-lang="toml">[[rules]]
name = &quot;nvim-c&quot;
match = &#39;^-c$&#39;
to = &quot;nvim-term&quot;
sync = true
passthrough = true
consumes = 1                     # -c とその次の argv を 1 セットで素通し

[[rules]]
name = &quot;nvim-p&quot;
match = &#39;^-[pOo]$&#39;
to = &quot;nvim-term&quot;
sync = true
passthrough = true
consumes_until = &#39;^[-+]&#39;         # 次のフラグが来るまで argv を吸い続ける
</code></pre>
<p><code>{{ passthrough }}</code> を <code>args</code> の中で参照すると、フラグを <strong>任意の位置に挿入</strong>できます。gvim のように <code>--remote-silent &lt;file&gt;</code> の前にフラグを置きたい、というケースに便利です。</p>
<pre><code data-lang="toml">[todoke.gvim]
command = &quot;gvim&quot;
gui = true
[todoke.gvim.args]
default = [
  &quot;--servername&quot;, &quot;{{ group | upper }}&quot;,
  &quot;{{ passthrough }}&quot;,                       # ← ここで素通しフラグを並べる
  &quot;--remote-silent&quot;, &quot;{{ input }}&quot;,
]
</code></pre>
<p><code>{{ passthrough }}</code> を<strong>単独の args 要素</strong>として書いた場合は <strong>inline 展開</strong>されるので、<code>-c :set ft=md</code> のような複数 argv のフラグも 1 つの <code>""</code> にまとめられず、ちゃんと argv 単位で渡ります。</p>
<p>ちなみに passthrough と並んで、引数を空白で連結した<strong>全体</strong>を 1 つの正規表現でマッチさせる <strong><code>joined = true</code></strong> というモードもあります。<code>+42 file.txt</code> のような「フラグとファイルを 1 ルールで丸ごと拾いたい」といった用途向けです。詳細は <a rel="external" href="https://github.com/yukimemi/todoke">README</a> を参照してください。</p>
<h2 id="[vars]_で_GUI_を切り替える"><code>[vars]</code> で GUI を切り替える</h2>
<p>Neovim の GUI フロントエンドには <code>neovide</code>、<code>nvim-qt</code>、素の <code>nvim</code> (ターミナル) などの選択肢があって、気分で切り替えたいことがあります。todoke の <code>[vars]</code> セクションと Tera テンプレートを組み合わせると、<strong>1 行書き換えるだけでフロントエンドを差し替えられる</strong>構成になります。</p>
<pre><code data-lang="toml">[vars]
# ここを書き換えるだけで全部追従する
gui = &quot;neovide&quot;
# CLI 引数を embedded nvim に渡すために `--` 区切りが必要なラッパー型 GUI 一覧
wrapper_guis = [&quot;neovide&quot;, &quot;nvim-qt&quot;]

[todoke.gui]
kind = &quot;neovim&quot;
command = &quot;{{ vars.gui }}&quot;
listen = &#39;{% if is_windows() %}\\.\pipe\nvim-todoke-{{ group }}{% else %}/tmp/nvim-todoke-{{ group }}.sock{% endif %}&#39;
# ラッパー GUI のときだけ gui = true (Windows の cmd ウィンドウを抑止)
# 素の nvim はターミナルが必要なので false にする
gui = {{ vars.gui in vars.wrapper_guis }}

{% if vars.gui in vars.wrapper_guis %}
# ラッパー型は本体 nvim への引数の前に `--` を挟む必要がある
[todoke.gui.args]
remote = [&quot;--&quot;]
{% endif %}

[[rules]]
match = &#39;.*&#39;
to = &quot;gui&quot;
mode = &quot;remote&quot;
</code></pre>
<p><code>vars.gui</code> を変えるだけで、</p>
<ul>
<li><code>"nvim"</code> → <code>nvim FILE --listen PIPE</code> (ターミナルで起動)</li>
<li><code>"neovide"</code> → <code>neovide FILE -- --listen PIPE</code> (GUI、<code>--</code> 区切りあり)</li>
<li><code>"nvim-qt"</code> → <code>nvim-qt FILE -- --listen PIPE</code> (GUI、<code>--</code> 区切りあり)</li>
</ul>
<p>の 3 通りに切り替わります。<code>{% if %}</code> の中で <strong><code>[todoke.gui.args]</code> テーブルそのものを条件包含できる</strong>のは、todoke が TOML パース前に全文を Tera に通している (= TOML 構造ごと条件分岐できる) ことの利点です。</p>
<p>新しいラッパー GUI を試したくなったら <code>wrapper_guis</code> に追加するだけで対応できる、というのも気に入っています。</p>
<h2 id="CLI">CLI</h2>
<p>todoke 自身のフラグはすべて <code>--todoke-</code> プレフィックスで、ロングオプションのみです。これは <code>todoke</code> が <code>$EDITOR</code> の代わりに呼ばれたときに、呼び出し元（nvim・vim・helix）のフラグと<strong>絶対に衝突しないようにするため</strong>の設計です。</p>
<pre><code>todoke [INPUTS]...                 # ルールに従ってディスパッチ (デフォルト)
todoke check [INPUTS]...           # ドライラン: 実行せずに dispatch plan を表示
todoke doctor                      # 設定の健康診断
todoke list [--alive-only]         # 起動中のインスタンス一覧 (※ v2.0.0 時点で未実装)
todoke kill &lt;GROUP&gt; | --all        # インスタンスを終了        (※ v2.0.0 時点で未実装)
todoke config path                 # 設定ファイルパスを表示
todoke config init                 # 無ければ default を書き出す (idempotent)
todoke config edit                 # $EDITOR で開く
todoke config show [--rendered]    # 中身を表示 (--rendered で Tera 展開後)
todoke completion &lt;shell&gt;          # シェル補完スクリプト
todoke --todoke-config &lt;PATH&gt;      # 設定ファイルを上書き
todoke --todoke-to &lt;NAME&gt;          # ルールを無視してターゲット強制
todoke --todoke-group &lt;NAME&gt;       # ルールを無視してグループ強制
todoke --todoke-as &lt;KIND&gt;          # 入力種別を file / url / raw に強制
todoke --todoke-verbose            # ログレベルを上げる (繰り返し可)
</code></pre>
<p><code>check</code> と <code>doctor</code> を用意したのは、ルールが増えるとどれが当たるか分からなくなる、というのが個人的な経験で確実だったからです。<code>todoke check ~/notes.md https://mail.google.com https://github.com/yukimemi</code> と打てば、それぞれどのルールが当たってどのターゲットに飛ぶかが<strong>実行抜き</strong>で確認できます。</p>
<h2 id="想定する使いどころ">想定する使いどころ</h2>
<h3 id="$EDITOR_として"><code>$EDITOR</code> として</h3>
<pre><code data-lang="sh">export EDITOR=todoke
git commit                         # editor-callback ルール → 新規 nvim sync
git rebase -i HEAD~3               # 同上
crontab -e                         # 同上 (CRONTAB 系のファイル名は default config で拾う)
</code></pre>
<h3 id="Windows_のファイル関連付けとして">Windows のファイル関連付けとして</h3>
<p><code>.txt</code> を右クリック → プログラムから開く → <code>todoke.exe</code> を選ぶだけです。<code>gui = true</code> のターゲットは <code>cmd /c start</code> を経由せずに起動するので、コンソールウィンドウが点滅しません。GUI な Neovim フロントエンド（neovide / nvim-qt）や VSCode をターゲットにするときに気持ちのよい体験になります。</p>
<h3 id="URL_/_独自_ID_のディスパッチャとして">URL / 独自 ID のディスパッチャとして</h3>
<p><code>firefox</code> や <code>chrome</code> の代わりに <code>todoke</code> を渡しておくと、ホストごとにブラウザを使い分けたり (Gmail は Chrome、GitHub は Edge、など)、URL のパスから別プロファイルに届けたり、というのが設定だけで作れます。</p>
<h2 id="設計のポイント">設計のポイント</h2>
<h3 id="batch_という単位">batch という単位</h3>
<p>todoke 内部では、入力をルール照合した結果、<strong>「同じターゲット × 同じグループに行く入力」が 1 つの batch にまとめられます</strong>。これによって <code>todoke a.md b.md c.md</code> を渡したときに、3 回別々に nvim へ RPC が飛ぶのではなく、1 回の <code>:args a.md b.md c.md</code> 相当でまとめて開く、という挙動が実現します。</p>
<p><code>passthrough</code> ルールがフラグを集めて、その batch にマージするのもこの単位です。<code>+42 a.txt b.txt</code> で <code>+42</code> は a.txt の batch に入るし、<code>a.txt b.txt</code> 全体に対して <code>+42</code> を効かせる挙動になります。</p>
<h3 id="Tera_を全箇所で使う">Tera を全箇所で使う</h3>
<p><code>command</code> / <code>listen</code> / <code>args</code> / <code>group</code> / <code>to</code> といった<strong>ルール側のあらゆる文字列</strong>が Tera で展開されます。これによって、</p>
<ul>
<li><code>listen = '{% if is_windows() %}\\.\pipe\...{% else %}/tmp/...{% endif %}'</code> のような OS 分岐</li>
<li><code>command = "{{ vars.gui }}"</code> のような変数経由の参照 ([vars] セクションで一括切り替え)</li>
<li><code>group = "{{ env.PROJECT_GROUP | default(value='default') }}"</code> のような環境変数経由の動的グループ</li>
</ul>
<p>がすべて宣言的な設定だけで書けます。</p>
<p>それに加えて、<strong>入力種別ごとに使える変数</strong>も用意されています。</p>
<ul>
<li><strong>file 入力</strong>: <code>{{ file_path }}</code> (正規化された絶対パス) / <code>{{ file_dir }}</code> / <code>{{ file_name }}</code> / <code>{{ file_stem }}</code> / <code>{{ file_ext }}</code> (拡張子、ドットなし)</li>
<li><strong>URL 入力</strong>: <code>{{ url_scheme }}</code> / <code>{{ url_host }}</code> / <code>{{ url_port }}</code> / <code>{{ url_path }}</code> / <code>{{ url_query }}</code> / <code>{{ url_fragment }}</code></li>
<li>共通: <code>{{ input }}</code> (生の入力文字列) と <code>{{ input_type }}</code>、ルールの <code>match</code> で取った正規表現キャプチャの <code>{{ cap.1 }}</code> / <code>{{ cap.&lt;name&gt; }}</code></li>
</ul>
<p>たとえば「<code>.xlsx</code> ファイルは Excel.exe に渡す」はこれだけで書けます。</p>
<pre><code data-lang="toml">[todoke.excel]
command = &quot;C:/Program Files/Microsoft Office/root/Office16/EXCEL.EXE&quot;
gui = true
args.default = [&quot;{{ file_path }}&quot;]    # 絶対パスに正規化済み

[[rules]]
match = &#39;\.xlsx?$&#39;
to = &quot;excel&quot;
</code></pre>
<p>URL 側も同じで、たとえば Edge を URL のホストごとに別プロファイルに分けたければ:</p>
<pre><code data-lang="toml">[todoke.edge-per-host]
command = &quot;msedge&quot;
gui = true
args.default = [
  &quot;--profile-directory={{ url_host }}&quot;,   # github.com / mail.google.com / ... ごとに別プロファイル
  &quot;{{ input }}&quot;,
]

[[rules]]
match = &#39;^https?://&#39;
input_type = &quot;url&quot;
to = &quot;edge-per-host&quot;
</code></pre>
<p><a rel="external" href="https://github.com/yukimemi/shun">shun</a> / <a rel="external" href="https://github.com/yukimemi/rvpm">rvpm</a> でやっていたのと同じ思想で、「設定は宣言的に、でもちょっとしたロジックも書けるように」という落としどころを TOML × Tera に求めた形です。</p>
<h3 id="append_inputs_/_append_passthrough_の_auto"><code>append_inputs</code> / <code>append_passthrough</code> の auto</h3>
<p><code>exec</code> ターゲットの <code>args</code> に <code>{{ input }}</code> や <code>{{ file_path }}</code> が出てくると、todoke はそれを検知して<strong>末尾への自動 append を抑制</strong>します。<code>{{ passthrough }}</code> についても同様です。これは設定が直感的に書けるようにするための小さな仕掛けで、たとえば</p>
<pre><code data-lang="toml">args.default = [&quot;--new-window&quot;, &quot;{{ input }}&quot;]
</code></pre>
<p>と書くだけで、URL が末尾に二重に付かなくなります。明示的に <code>append_inputs = false</code> を書くこともできますが、9 割のケースでは auto 検知だけで済みます。</p>
<h2 id="ロードマップ">ロードマップ</h2>
<p>リリース済み (v2.0.0):</p>
<ul>
<li>コアディスパッチ、neovim / 汎用 exec バックエンド、<code>$EDITOR</code> 互換、Windows のファイル関連付け対応、カラー出力</li>
<li><code>check</code> (dispatch plan のドライラン)、<code>doctor</code> (設定の静的解析)、<code>completion</code></li>
<li><code>config</code> サブコマンド一式 — <code>path</code> / <code>init</code> / <code>edit</code> / <code>show</code></li>
</ul>
<p>予定:</p>
<ul>
<li><code>list</code> / <code>kill</code> — 現状はスタブ (<code>bail!("not implemented yet")</code>)。起動中の nvim インスタンス一覧 / グループ単位での終了</li>
<li>neovim での <code>remote + sync</code> (<code>nvim_buf_attach</code> 経由) — 再利用 nvim のバッファが閉じるまでブロックする (現状は fresh-spawn の nvim でしか <code>sync = true</code> が効かない)</li>
<li><code>script</code> ターゲット kind — 任意のシェルコマンドをハンドラにできるようにして、todoke を「ファイル種別ごとの汎用 open-with」ツールとして使えるようにする</li>
</ul>
<h2 id="おわりに">おわりに</h2>
<p><code>$EDITOR=todoke</code> にしてからは、<code>git commit</code> でも、ファイルマネージャからのダブルクリックでも、URL を渡しても、すべて 1 個の todoke コマンドが然るべき相手に届けてくれるので、エディタまわりの取り回しがだいぶ楽になりました。</p>
<p><code>rvpm</code> のときに使った Rust + Tera + Tokio の組み合わせに今回も助けられていて、設定が宣言的に書ける CLI ツールを少ないコードで作れる構成として、自分の中で定番化しつつあります。</p>
<p>Neovim を <code>$EDITOR</code> にしているけど <code>git commit</code> の体験が微妙、ファイルマネージャから開いたファイルが新しい nvim を立ち上げてしまうのが嫌、URL や独自 ID をシェルから直接ブラウザに飛ばしたい、といった嗜好に合う方は、ぜひ試してみてください — <strong>君に届け！</strong></p>
<a href="https://github.com/yukimemi/todoke" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/todoke: A rule-driven file and URL dispatcher: hands incoming paths (or URLs) to the right handler based on TOML-defined rules.</div><div class="link-card-description">A rule-driven file and URL dispatcher: hands incoming paths (or URLs) to the right handler based on TOML-defined rules. - yukimemi/todoke</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/4935e4fad1ce8170e9fce9beb92ccd147b641127aedfbc8d4fb3dbe0e7aefe52/yukimemi/todoke')"></div></a>
]]></content:encoded>
      </item>
      <item>
          <title>Rust 製の事前コンパイル型 Neovim プラグインマネージャー rvpm を作った</title>
          <link>https://yukimemi.pages.dev/posts/rvpm/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/rvpm/</guid>
          <pubDate>Sun, 19 Apr 2026 12:00:00 GMT</pubDate>
          <description>CLI ファーストで `loader.lua` を事前コンパイルする Rust 製 Neovim プラグインマネージャー rvpm を作りました。dvpm からの移行先として、遅延ロードと起動速度の両立を狙っています。</description>
          <content:encoded><![CDATA[<p>自作の Neovim プラグインマネージャー <strong>rvpm</strong> を <a rel="external" href="https://claude.com/claude-code">Claude Code</a> と一緒に作りました。Rust 製で、設定ファイル (<code>config.toml</code>) から静的な <code>loader.lua</code> を事前コンパイルする CLI ファーストな設計になっています。</p>
<a href="https://github.com/yukimemi/rvpm" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/rvpm: Fast Neovim plugin manager with pre-compiled loader and merge optimization</div><div class="link-card-description">Fast Neovim plugin manager with pre-compiled loader and merge optimization - yukimemi/rvpm</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/c753e1a0da89f29463fc508174083d637428c2c0eb2288869f8cc7ff396389e1/yukimemi/rvpm')"></div></a>
<img src="https://raw.githubusercontent.com/yukimemi/rvpm/main/vhs/demo.gif" alt="rvpm demo" width="720" />
<p><code>rvpm profile</code> でプラグイン単位の Neovim 起動時間を phase ごとに可視化する TUI も付いています。</p>
<img src="https://raw.githubusercontent.com/yukimemi/rvpm/main/vhs/profile.gif" alt="rvpm profile TUI" width="720" />
<h2 id="なぜ新しく作ったのか">なぜ新しく作ったのか</h2>
<p>以前、Deno + denops ベースの <a rel="external" href="https://github.com/yukimemi/dvpm">dvpm</a> を作り、愛用していました（<a href="/posts/dvpm/">過去の記事</a>・<a href="/posts/dvpm_lazy_cache/">キャッシュ機能の記事</a>）。dvpm は denops との親和性も高く気に入っていたのですが、使い続けるうちにいくつか気になる点が出てきました。</p>
<ul>
<li><strong>設定を TypeScript で書く</strong>前提なので、素の Neovim 設定と混ぜるときに壁がある</li>
<li>denops 起動前には遅延ロードが効かず、キャッシュ機能でカバーしていた</li>
<li>AI 時代、プラグインの追加・更新・削除といった管理操作はエディタの外からコマンドで叩けたほうが何かと都合が良い</li>
</ul>
<p>ちょうど別プロジェクトで Rust を触っていたので、<strong>「設定は TOML、管理は CLI、Neovim 側はただ <code>loader.lua</code> を読むだけ」</strong> という形を Rust で作ってみることにしました。</p>
<h2 id="rvpm_の特徴">rvpm の特徴</h2>
<ul>
<li><strong>CLI ファースト</strong> — プラグインの追加・更新・削除はすべてターミナルで完結</li>
<li><strong>TOML 設定</strong> — 宣言的で、<a rel="external" href="https://keats.github.io/tera/">Tera</a> テンプレート対応</li>
<li><strong>事前コンパイルされた <code>loader.lua</code></strong> — <code>rvpm generate</code> 時にプラグインディレクトリを walk してファイルリストを焼き込む。Neovim は固定の <code>dofile()</code> / <code>source</code> を並べるだけ</li>
<li><strong>豊富な遅延ロードトリガー</strong> — <code>on_cmd</code>, <code>on_ft</code>, <code>on_map</code>, <code>on_event</code>, <code>on_path</code>, <code>on_source</code>, <code>ColorSchemePre</code> 自動検知、<code>depends</code> の解決まで実施</li>
<li><strong>merge 最適化</strong> — <code>merge = true</code> のプラグインは単一の <code>runtimepath</code> エントリに集約</li>
<li><strong>プラグインブラウザ TUI</strong> — <code>rvpm browse</code> で GitHub の <code>neovim-plugin</code> トピックを眺めながらインストール</li>
<li><strong>AI CLI 連携</strong> — <code>rvpm add</code> / <code>rvpm tune</code> で Claude / Gemini / Codex に <code>[[plugins]]</code> ブロックとフックファイル一式を設計させられる</li>
<li><strong>エディタ起動への影響を抑える</strong> — CLI で管理する別プロセスなので、循環依存・存在しないプラグイン・設定ミスがあっても Neovim の起動を巻き添えにしにくい（rvpm 側は警告で止めない）</li>
</ul>
<h2 id="インストール">インストール</h2>
<pre><code data-lang="sh"># crates.io から
cargo install rvpm

# または最新 main から
cargo install --git https://github.com/yukimemi/rvpm
</code></pre>
<p><a rel="external" href="https://github.com/yukimemi/rvpm/releases">Releases</a> ページに Linux (x86_64) / macOS (Intel / Apple Silicon) / Windows (x86_64) のビルド済みバイナリも置いてあります。</p>
<h2 id="クイックスタート">クイックスタート</h2>
<pre><code data-lang="sh"># 1. 初期化 (config.toml を作り、init.lua に loader の読み込みを書き込む)
rvpm init --write

# 2. プラグインを追加
rvpm add folke/snacks.nvim
rvpm add nvim-telescope/telescope.nvim

# 3. GitHub の &quot;neovim-plugin&quot; トピックを TUI で探してインストール
rvpm browse

# 4. インストール済みプラグインを TUI で管理
rvpm list

# 5. config.toml を開いて細かい設定を書く
rvpm config
</code></pre>
<h3 id="既存の_Neovim_設定を壊さずに試す">既存の Neovim 設定を壊さずに試す</h3>
<p>rvpm は Neovim の <code>$NVIM_APPNAME</code> に対応しているので、既存の <code>~/.config/nvim/</code> をそのまま残した状態で、別の appname で試せます。</p>
<pre><code data-lang="sh"># Bash / Zsh
NVIM_APPNAME=nvim-rvpm rvpm init --write
NVIM_APPNAME=nvim-rvpm rvpm add folke/snacks.nvim
NVIM_APPNAME=nvim-rvpm nvim
</code></pre>
<pre><code data-lang="powershell"># PowerShell
$env:NVIM_APPNAME = &quot;nvim-rvpm&quot;
rvpm init --write
rvpm add folke/snacks.nvim
nvim
</code></pre>
<p>関係ファイルは次の 3 つのディレクトリに隔離されるので、気に入らなければこれらを削除するだけで跡形もなく消せます。</p>
<ul>
<li><code>~/.config/nvim-rvpm/</code> — Neovim 側の設定ディレクトリ（<code>rvpm init --write</code> が <code>init.lua</code> をここに書き込む）</li>
<li><code>~/.config/rvpm/nvim-rvpm/</code> — rvpm の config_root（<code>config.toml</code>、グローバル <code>before.lua</code> / <code>after.lua</code>）</li>
<li><code>~/.cache/rvpm/nvim-rvpm/</code> — rvpm の cache_root（plugin の clone と <code>loader.lua</code>）</li>
</ul>
<h2 id="設定例">設定例</h2>
<p><code>~/.config/rvpm/&lt;appname&gt;/config.toml</code>:</p>
<pre><code data-lang="toml">[options]
concurrency = 10       # git 並列数 (デフォルト 8)
auto_clean  = true     # sync / generate で未参照プラグインを自動削除
url_style   = &quot;full&quot;   # config.toml に書くときは full URL で

# 即時ロード (on_* トリガーなし)
[[plugins]]
url = &quot;folke/snacks.nvim&quot;

# on_cmd があるので lazy = true が自動推論される
# depends は &quot;このプラグインより先に読み込む&quot; 依存関係
[[plugins]]
url     = &quot;nvim-telescope/telescope.nvim&quot;
depends = [&quot;plenary.nvim&quot;]
on_cmd  = [&quot;Telescope&quot;]

# ファイルタイプ + イベントで遅延ロード
[[plugins]]
url      = &quot;neovim/nvim-lspconfig&quot;
on_ft    = [&quot;rust&quot;, &quot;toml&quot;, &quot;lua&quot;]
on_event = [&quot;BufReadPre&quot;]

# キーマップで遅延ロード (mode と desc も指定可)
[[plugins]]
url  = &quot;folke/which-key.nvim&quot;
on_map = [
  &quot;&lt;leader&gt;?&quot;,
  { lhs = &quot;&lt;leader&gt;v&quot;, mode = [&quot;n&quot;, &quot;x&quot;], desc = &quot;Visual leader&quot; },
]

# &quot;他のプラグインの読み込み完了&quot; を遅延トリガーに使う (on_source)
# snacks.nvim がロードされて rvpm_loaded_snacks.nvim が発火したら読み込む
[[plugins]]
url       = &quot;folke/todo-comments.nvim&quot;
on_source = [&quot;snacks.nvim&quot;]
</code></pre>
<p><code>on_*</code> のどれか一つでも指定されていれば <code>lazy</code> は自動で <code>true</code> になります。</p>
<h3 id="遅延ロードトリガー">遅延ロードトリガー</h3>
<table><thead><tr><th>キー</th><th>意味</th></tr></thead><tbody>
<tr><td><code>on_cmd</code></td><td><code>:Foo</code> 実行で読み込む。bang・range・count・補完を保持</td></tr>
<tr><td><code>on_ft</code></td><td><code>FileType</code> イベント。発火後に再トリガーして <code>ftplugin/</code> を走らせる</td></tr>
<tr><td><code>on_event</code></td><td>Neovim イベント。<code>"User Xxx"</code> は <code>pattern = "Xxx"</code> に自動展開</td></tr>
<tr><td><code>on_path</code></td><td><code>BufRead</code> / <code>BufNewFile</code> のグロブ一致</td></tr>
<tr><td><code>on_source</code></td><td>別プラグインが発火する <code>rvpm_loaded_&lt;name&gt;</code> User autocmd で読み込む</td></tr>
<tr><td><code>on_map</code></td><td>キーマップ。文字列・テーブル（<code>lhs</code> / <code>mode</code> / <code>desc</code>）の両対応</td></tr>
</tbody></table>
<p><code>lazy.nvim</code> など主要なプラグインマネージャーが持っている遅延ロードトリガーには概ね対応しています。</p>
<h3 id="Colorscheme_の遅延ロード">Colorscheme の遅延ロード</h3>
<p><code>lazy = true</code> なプラグインが <code>colors/*.vim</code> や <code>colors/*.lua</code> を持っていると、<code>ColorSchemePre</code> のハンドラを <code>rvpm generate</code> 時に<strong>自動で</strong>登録します。設定で明示する必要はありません。</p>
<pre><code data-lang="toml">[[plugins]]
url  = &quot;folke/tokyonight.nvim&quot;
lazy = true  # colors/ を持っているので ColorSchemePre が自動登録される

[[plugins]]
url  = &quot;catppuccin/nvim&quot;
name = &quot;catppuccin&quot;
lazy = true
</code></pre>
<p>複数のカラースキームを入れていても、起動コストはゼロで <code>:colorscheme tokyonight</code> のタイミングで初めて読み込まれます。</p>
<h3 id="Tera_テンプレート">Tera テンプレート</h3>
<p><code>config.toml</code> 全体が TOML パース前に <a rel="external" href="https://keats.github.io/tera/">Tera</a> で処理されるので、条件分岐や環境変数の参照ができます。</p>
<pre><code data-lang="toml">[vars]
use_blink = true
use_cmp   = false

{% if vars.use_blink %}
[[plugins]]
url = &quot;saghen/blink.cmp&quot;
on_event = [&quot;InsertEnter&quot;, &quot;CmdlineEnter&quot;]
{% endif %}

{% if vars.use_cmp %}
[[plugins]]
url = &quot;hrsh7th/nvim-cmp&quot;
on_event = &quot;InsertEnter&quot;
{% endif %}

{% if is_windows %}
[[plugins]]
url = &quot;thinca/vim-winenv&quot;
{% endif %}
</code></pre>
<p><code>{% if %}</code> は「そもそも <code>loader.lua</code> に含めない」、<code>cond = "..."</code> は「含めるが実行時に Lua で判定」、という使い分けになっています。</p>
<h2 id="コマンド一覧">コマンド一覧</h2>
<table><thead><tr><th>コマンド</th><th>動作</th></tr></thead><tbody>
<tr><td><code>rvpm sync [--prune]</code></td><td>plugin を clone / pull して <code>loader.lua</code> を再生成</td></tr>
<tr><td><code>rvpm generate</code></td><td><code>loader.lua</code> のみ再生成（git 操作なし）</td></tr>
<tr><td><code>rvpm clean</code></td><td><code>config.toml</code> に無い plugin ディレクトリを削除</td></tr>
<tr><td><code>rvpm add &lt;repo&gt;</code></td><td>plugin を追加して sync</td></tr>
<tr><td><code>rvpm update [query]</code></td><td>plugin を git pull</td></tr>
<tr><td><code>rvpm remove [query]</code></td><td><code>config.toml</code> から plugin を削除してディレクトリも除去</td></tr>
<tr><td><code>rvpm edit [query] [--init|--before|--after] [--global]</code></td><td>plugin ごとの Lua フックを <code>$EDITOR</code> で編集</td></tr>
<tr><td><code>rvpm set [query] ...</code></td><td><code>lazy</code> / <code>merge</code> / <code>on_*</code> / <code>rev</code> などを CLI から変更</td></tr>
<tr><td><code>rvpm config</code></td><td><code>config.toml</code> を <code>$EDITOR</code> で開く</td></tr>
<tr><td><code>rvpm init [--write]</code></td><td><code>loader.lua</code> を <code>init.lua</code> に繋ぎ込む</td></tr>
<tr><td><code>rvpm list [--no-tui]</code></td><td>plugin を TUI で管理</td></tr>
<tr><td><code>rvpm browse</code></td><td>GitHub <code>neovim-plugin</code> topic を TUI で閲覧</td></tr>
</tbody></table>
<h3 id="rvpm_list_—_管理_TUI"><code>rvpm list</code> — 管理 TUI</h3>
<p>インストール済みプラグインの一覧表示と、そこからの操作を担う TUI です。</p>
<table><thead><tr><th>キー</th><th>動作</th></tr></thead><tbody>
<tr><td><code>S</code></td><td>全プラグイン sync</td></tr>
<tr><td><code>u</code> / <code>U</code></td><td>選択プラグインを update / 全プラグインを update</td></tr>
<tr><td><code>d</code></td><td>選択プラグインを削除</td></tr>
<tr><td><code>e</code></td><td>プラグイン固有フックを編集 (<code>init.lua</code> / <code>before.lua</code> / <code>after.lua</code>)</td></tr>
<tr><td><code>s</code></td><td>プラグインオプションを変更 (<code>lazy</code> / <code>merge</code> / <code>on_*</code> など)</td></tr>
<tr><td><code>c</code></td><td><code>config.toml</code> を <code>$EDITOR</code> で開く</td></tr>
<tr><td><code>b</code></td><td><code>rvpm browse</code> に切替</td></tr>
<tr><td><code>/</code> <code>n</code> <code>N</code></td><td>インクリメンタル検索</td></tr>
<tr><td><code>j</code> <code>k</code> <code>g</code> <code>G</code> <code>Ctrl-d</code> <code>Ctrl-u</code></td><td>Vim ライクな移動</td></tr>
<tr><td><code>?</code></td><td>ヘルプ</td></tr>
</tbody></table>
<p>詳細は <a rel="external" href="https://github.com/yukimemi/rvpm">README</a> を参照してください。</p>
<h3 id="rvpm_browse_—_プラグイン探索_TUI"><code>rvpm browse</code> — プラグイン探索 TUI</h3>
<p>GitHub API から <code>neovim-plugin</code> topic のリポジトリ（最大 ~300 件）を取得して、plugin 一覧と README プレビューの 2 ペインで表示します。端末の横幅が広ければ左右に、狭ければ上下にレイアウトが自動で切り替わります。</p>
<table><thead><tr><th>キー</th><th>動作</th></tr></thead><tbody>
<tr><td><code>Tab</code></td><td>リスト ↔ README のフォーカス切替</td></tr>
<tr><td><code>Enter</code></td><td>選択中のプラグインを <code>config.toml</code> に追加</td></tr>
<tr><td><code>/</code></td><td>ローカルインクリメンタル検索 (name + description + topics)</td></tr>
<tr><td><code>S</code></td><td>GitHub API で再検索 (<code>topic:neovim-plugin &lt;query&gt;</code>)</td></tr>
<tr><td><code>s</code></td><td>ソート切替 (<code>stars</code> / <code>updated</code> / <code>name</code>)</td></tr>
<tr><td><code>o</code></td><td>プラグインの GitHub ページをブラウザで開く</td></tr>
<tr><td><code>l</code></td><td><code>rvpm list</code> に切替</td></tr>
<tr><td><code>R</code></td><td>検索キャッシュをクリアして再取得</td></tr>
</tbody></table>
<p>既にインストール済みのプラグインには緑の <code>✓</code> が付き、Enter してもインストール済みである旨の警告だけが出て重複追加を防ぎます。検索結果は一定期間キャッシュされるので、同じ画面を何度も往復してもレスポンスは軽快です。</p>
<p>README プレビューは内蔵の markdown レンダラ（<code>tui-markdown</code>）で描画していますが、オプションで <code>mdcat</code> / <code>glow</code> / <code>bat</code> など外部レンダラにパイプすることもできます。</p>
<pre><code data-lang="toml">[options.browse]
readme_command = [&quot;mdcat&quot;]
# readme_command = [&quot;glow&quot;, &quot;-s&quot;, &quot;dark&quot;, &quot;-w&quot;, &quot;{{ width }}&quot;, &quot;{{ file_path }}&quot;]
</code></pre>
<h2 id="ディレクトリ構成とユーザー設定">ディレクトリ構成とユーザー設定</h2>
<p>rvpm のファイル配置は次のようになっています。<code>&lt;appname&gt;</code> は <code>$RVPM_APPNAME</code> → <code>$NVIM_APPNAME</code> → <code>"nvim"</code> の順で解決されるので、Neovim の appname 切り替えとそのまま揃います。</p>
<pre><code data-lang="text">~/.config/rvpm/&lt;appname&gt;/                    ← config_root
├── config.toml                              ← プラグイン宣言と [options]
├── before.lua                               ← グローバル before (ユーザー設定前半)
├── after.lua                                ← グローバル after  (ユーザー設定後半)
└── plugins/&lt;host&gt;/&lt;owner&gt;/&lt;repo&gt;/           ← プラグイン固有のフック
    ├── init.lua                             ← rtp 追加前
    ├── before.lua                           ← rtp 追加後、plugin/* ソース前
    └── after.lua                            ← plugin/* ソース後

~/.cache/rvpm/&lt;appname&gt;/                     ← cache_root
├── plugins/
│   ├── repos/&lt;host&gt;/&lt;owner&gt;/&lt;repo&gt;/         ← plugin の clone 先
│   ├── merged/                              ← merge=true の共通 rtp
│   └── loader.lua                           ← 生成されるローダー
└── browse/                                  ← `rvpm browse` のキャッシュ
</code></pre>
<p>Windows でも <code>%APPDATA%</code> ではなく <code>%USERPROFILE%\.config\...</code> / <code>%USERPROFILE%\.cache\...</code> に配置されます。dotfiles をそのまま持ち運べる構造にしたかったのでこうしています。</p>
<h3 id="ユーザー自身の設定も_rvpm_に任せられる">ユーザー自身の設定も rvpm に任せられる</h3>
<p>rvpm の考え方として<strong>ユーザーの Neovim 設定そのものも rvpm の管理下に置く</strong>、という使い方にしています。<code>init.lua</code> にはローダーの読み込み一行だけを書き、それ以外のユーザー設定は <code>config_root/</code> 直下の <code>before.lua</code> / <code>after.lua</code> に分けて置く形です。</p>
<table><thead><tr><th>ファイル</th><th>フェーズ</th><th>用途</th></tr></thead><tbody>
<tr><td><code>~/.config/nvim/init.lua</code></td><td>—</td><td><code>loader.lua</code> を <code>dofile(...)</code> するだけ（<code>rvpm init --write</code> で自動生成）</td></tr>
<tr><td><code>{config_root}/before.lua</code></td><td>Phase 3</td><td>プラグインより<strong>先</strong>に走らせたい設定 (<code>vim.g.*</code> など)</td></tr>
<tr><td><code>{config_root}/after.lua</code></td><td>Phase 9</td><td>プラグインが揃った<strong>後</strong>に走らせたい設定（キーマップ、カラースキーム適用など）</td></tr>
</tbody></table>
<p>これらは設定エントリなしで <code>rvpm generate</code> 時に自動検出されます。同じ仕組みでプラグイン固有のフックも <code>plugins/&lt;host&gt;/&lt;owner&gt;/&lt;repo&gt;/</code> に <code>init.lua</code> / <code>before.lua</code> / <code>after.lua</code> を置くだけで拾われます。</p>
<pre><code data-lang="lua">-- ~/.config/rvpm/nvim/plugins/github.com/nvim-telescope/telescope.nvim/after.lua
require(&quot;telescope&quot;).setup({
  defaults = { layout_strategy = &quot;vertical&quot; },
})
vim.keymap.set(&quot;n&quot;, &quot;&lt;leader&gt;ff&quot;, &quot;&lt;cmd&gt;Telescope find_files&lt;cr&gt;&quot;)
</code></pre>
<p><code>config.toml</code>・グローバルフック・プラグイン固有フックを全部同じツリーの下に置けるので、dotfiles として <code>~/.config/rvpm/</code> ごと管理すれば Neovim 設定一式が rvpm にまとまります。</p>
<h2 id="AI_による_rvpm_add_—_Claude_/_Gemini_/_Codex_連携">AI による <code>rvpm add</code> — Claude / Gemini / Codex 連携</h2>
<p><code>rvpm add</code> には、追加するプラグインの <code>[[plugins]]</code> ブロックの設計まるごと AI CLI に任せるモードがあります。<code>options.ai = "claude"</code> と書く（または <code>--ai claude</code> を都度指定する）だけで、プラグインの README / <code>doc/</code> を読み込んだ AI が <code>[[plugins]]</code> エントリと per-plugin の <code>init.lua</code> / <code>before.lua</code> / <code>after.lua</code> までセットで提案してきます。</p>
<p><img src="/static/images/2026-04-19_rvpm_ai_add.png" alt="rvpm AI add" /></p>
<pre><code data-lang="toml">[options]
ai = &quot;claude&quot;           # &quot;off&quot; (default) | &quot;claude&quot; | &quot;gemini&quot; | &quot;codex&quot;
ai_language = &quot;ja&quot;      # 説明文を日本語で返してもらう
</code></pre>
<p><code>rvpm add owner/repo</code> 実行時の流れはこんな感じです。</p>
<ol>
<li>プラグインを clone（通常の add と同じ）</li>
<li>「rvpm の TOML スキーマ」「プラグインの README + <code>doc/</code>」「現在の <code>config.toml</code> と <code>plugins/</code> ツリー」「既存の per-plugin フックファイル」をプロンプトに組み立てる</li>
<li>設定された AI CLI を <code>claude -p</code> / <code>gemini -p</code> / <code>codex exec</code> で one-shot 起動</li>
<li>返ってきた XML（<code>&lt;rvpm:plugin_entry&gt;</code> / <code>&lt;rvpm:init_lua&gt;</code> / <code>&lt;rvpm:before_lua&gt;</code> / <code>&lt;rvpm:after_lua&gt;</code> / <code>&lt;rvpm:explanation&gt;</code>）をパース</li>
<li>提案を表示して <strong>Apply / Chat / Hand off / Skip</strong> を選ばせる</li>
</ol>
<p><strong>Chat</strong> を選ぶと「depends に plenary を追加して」「これは eager で読み込みたい」のような要望をワンラインで投げて再提案させられます。<strong>Hand off</strong> を選ぶとここまでのやり取りを一時ファイルに保存して、その CLI のインタラクティブセッションに制御を渡します。AI CLI 側のファイル編集ツール（Claude Code なら Edit / Write）でそのまま続きを進められます。</p>
<p><code>rvpm add</code> は新規追加時にしか動かないので、既に <code>config.toml</code> に登録済みのプラグインを AI に再設計させたい場合は <code>rvpm tune &lt;query&gt;</code> を使います。<code>tune</code> ではセクション単位で <strong>Use FRESH</strong>（クリーンな再設計で上書き）/ <strong>Use MERGED</strong>（既存の編集を残しつつ提案を取り込む）/ <strong>Keep existing</strong>（変更しない）が選べます。</p>
<h3 id="なぜこの機能が_AI_と相性良く成立するのか">なぜこの機能が AI と相性良く成立するのか</h3>
<p>これがこの記事で一番書きたかった話です。AI add を雑な思いつきで載せたわけではなく、rvpm のディレクトリ構造とフックのルールが固定化されているからこそ成立しています。</p>
<ul>
<li>プラグイン側の設定は <code>[[plugins]]</code> ブロックという単一のスキーマに集約されている。<code>on_*</code> トリガーや <code>lazy</code> / <code>merge</code> の意味は TOML 上で完結していて、Lua をどこに書くべきかで悩む余地がない</li>
<li>per-plugin のフックは <code>{config_root}/plugins/&lt;host&gt;/&lt;owner&gt;/&lt;repo&gt;/</code> 以下の <code>init.lua</code> / <code>before.lua</code> / <code>after.lua</code> の <strong>3 ファイルだけ</strong>。それぞれが走るタイミング（rtp 追加前 / <code>plugin/*</code> ソース前 / <code>plugin/*</code> ソース後）も 9 フェーズのローダーモデル上で固定</li>
<li>グローバルフックも <code>{config_root}/before.lua</code> / <code>after.lua</code> の <strong>2 つに限定</strong></li>
</ul>
<p>この「置き場所と意味が一意に決まっている」ことのおかげで、</p>
<ul>
<li>AI に渡すプロンプトをテンプレ化できる（スキーマと既存ファイルの内容をそのまま埋め込めば過不足ない）</li>
<li>AI 側も「どのフックに何を書けば、いつ走るか」を曖昧さなく出力できる</li>
<li>Apply 時に「<code>[[plugins]]</code> だけ FRESH を採用、<code>after.lua</code> は MERGED、<code>before.lua</code> は Keep」のような <strong>per-section の選択肢</strong>が成立する</li>
</ul>
<p>逆にここがフリーフォーム（<code>init.lua</code> 一枚に何でも書く方式）だと、AI の出力もコンテキストに強く依存して再現性が下がりますし、ユーザーの既存設定との突き合わせも難しくなります。<strong>「リポジトリパスでフックファイルを分離する」という構造的な制約が、AI 時代になって思いがけず効いてきた</strong>、という話でした。</p>
<h2 id="設計のポイント">設計のポイント</h2>
<h3 id="9_フェーズのローダーモデル">9 フェーズのローダーモデル</h3>
<p><code>rvpm generate</code> が吐く <code>loader.lua</code> は次の 9 フェーズで構成されています。</p>
<pre><code data-lang="text">Phase 1: vim.go.loadplugins = false            -- Neovim の自動 source を抑止
Phase 2: load_lazy ヘルパ                       -- 遅延プラグインを実行時に読み込む関数
Phase 3: global before.lua                     -- ユーザーの before フック
Phase 4: 全プラグインの init.lua                 -- 依存順 / rtp 追加前
Phase 5: rtp:append(merged_dir)                -- merge=true 用の単一 rtp
Phase 6: eager プラグイン (依存順):
            rtp 追加 → before.lua
            plugin/**/*.{vim,lua} を source
            ftdetect/** を source
            after/plugin/** を source
            after.lua
            User autocmd &quot;rvpm_loaded_&lt;name&gt;&quot; 発火   -- on_source の待ち受け先
Phase 7: 遅延トリガー登録                        -- on_cmd / on_ft / on_map / ...
Phase 8: ColorSchemePre ハンドラ                 -- colors/ がある lazy プラグイン用
Phase 9: global after.lua
</code></pre>
<p><code>plugin/</code> / <code>ftdetect/</code> / <code>after/plugin/</code> のファイルリストは <code>rvpm generate</code> のタイミングで glob して <code>loader.lua</code> に焼き込んでいます。
そのため Neovim 起動時は glob が走らず、固定の <code>dofile()</code> / <code>source</code> を順次読み込むだけです。
I/O コストは CLI 側で払っておいて、エディタ起動側はそれを静的に消費するだけ、という分業にしたかったのがこの設計の出発点でした。</p>
<p>各フェーズが実測でどれくらいかかっているかは、冒頭で紹介した <code>rvpm profile</code> の TUI で phase ごと・プラグイン単位に可視化できます。</p>
<h3 id="merge_最適化">merge 最適化</h3>
<p><code>merge = true</code> のプラグインは、<code>{cache_root}/plugins/merged/</code> に集約されて単一の <code>vim.opt.rtp:append(merged_dir)</code> になります。eager なプラグインをどれだけ積んでも <code>&amp;runtimepath</code> が膨らまないのが利点です。</p>
<h3 id="chezmoi_統合">chezmoi 統合</h3>
<p>自分は dotfiles を <a rel="external" href="https://www.chezmoi.io/">chezmoi</a> で管理しているので、専用のオプションも追加しました。
<code>options.chezmoi = true</code> にするとすべての書き込み（<code>config.toml</code>、グローバルフック、プラグイン固有フック）を <strong>chezmoi の source 側</strong>に行い、その後 <code>chezmoi apply --force &lt;target&gt;</code> で target に反映するフローに切り替わります。</p>
<pre><code data-lang="toml">[options]
chezmoi = true
</code></pre>
<h2 id="技術的なポイント">技術的なポイント</h2>
<h3 id="Rust">Rust</h3>
<p>Rust のエコシステムに乗せたことで得られたものが二つあります。</p>
<p>一つは <a rel="external" href="https://keats.github.io/tera/">Tera</a> テンプレートエンジンです。
TOML という宣言的なフォーマットに寄せつつ、<code>{% if %}</code> による条件分岐や <code>{{ vars.xxx }}</code> / <code>{{ env.XXX }}</code> / <code>{{ is_windows }}</code> での変数展開が挟めるので、「設定は宣言的に、でもちょっとしたロジックは書きたい」という欲張りな要求を満たせています。dvpm 時代に TypeScript だから自由にできていた部分を、TOML に落としても失わずに済んだ形です。</p>
<p>もう一つは <a rel="external" href="https://ratatui.rs/">ratatui</a> による TUI の表現力の部分です。
<code>rvpm sync</code> の進捗表示や <code>rvpm list</code> / <code>rvpm browse</code> のペイン分割、リアルタイムに更新される clone 状態など、ターミナル上でそれなりに見栄えのする画面が作れました。CLI ファーストを掲げるなら画面もちゃんと作り込みたかったので、ratatui の表現力に助けられています。</p>
<h3 id="前身_dvpm_との関係">前身 dvpm との関係</h3>
<p>rvpm は <a rel="external" href="https://github.com/yukimemi/dvpm">dvpm</a> の後継プロジェクトという位置付けです。dvpm は denops ベースで「TypeScript で Vim/Neovim の設定を書く」という尖った方向性でしたが、rvpm は <strong>「Neovim の標準的な Lua 設定に CLI で補助輪を付ける」</strong> という立ち位置を取っています。</p>
<p>設計面では、<a rel="external" href="https://github.com/vim-volt/volt">volt</a> から影響を受けています。(けっこう好きでした)
volt は Go 製の CLI プラグインマネージャーで、<code>$VOLTPATH/plugconf/&lt;host&gt;/&lt;owner&gt;/&lt;repo&gt;.vim</code> というリポジトリ URL そのままのパスに各プラグインの設定ファイルを置く、という発想を持っていました。
rvpm の <code>{config_root}/plugins/&lt;host&gt;/&lt;owner&gt;/&lt;repo&gt;/</code> 以下に <code>init.lua</code> / <code>before.lua</code> / <code>after.lua</code> を配置する仕組みは、ここから直接影響を受けています。
「プラグインごとの設定を vimrc (init.lua) から切り離して、リポジトリパスに対応したファイルとして独立管理する」という思想そのものが volt 由来です。
<a rel="external" href="https://github.com/folke/lazy.nvim">lazy.nvim</a> からも影響を受けていて、遅延ロードトリガーの命名（<code>on_cmd</code> / <code>on_ft</code> / <code>on_event</code> / <code>on_map</code>）はそちらに寄せました。</p>
<p>volt の「CLI で完結」「リポジトリパスで plugin 設定を分離」「ユーザーの 設定自体もマネージャ管理下に置く」という哲学と、lazy.nvim の遅延ロード API 設計、そして dvpm で積んできた自前のプラグインマネージャー実装の経験 — この三つが rvpm のベースになっています。</p>
<h2 id="おわりに">おわりに</h2>
<p>自分で使うためのツールなのでドッグフーディングは毎日しています。自分の dotfiles も <a rel="external" href="https://github.com/yukimemi/dotfiles/tree/main/dot_config/rvpm/nvim"><code>~/.config/rvpm/nvim/</code></a> に移行済みで、200 個近いプラグインを rvpm で管理しています。</p>
<p>Neovim プラグインマネージャー自体は世の中に優秀なものが既にたくさんありますが、「CLI でプラグイン管理を完結させたい」といった嗜好に合う方がいれば、ぜひ試してみてください！</p>
<a href="https://github.com/yukimemi/rvpm" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/rvpm: Fast Neovim plugin manager with pre-compiled loader and merge optimization</div><div class="link-card-description">Fast Neovim plugin manager with pre-compiled loader and merge optimization - yukimemi/rvpm</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/c753e1a0da89f29463fc508174083d637428c2c0eb2288869f8cc7ff396389e1/yukimemi/rvpm')"></div></a>
]]></content:encoded>
      </item>
      <item>
          <title>キーボードドリブンなミニマルランチャー shun (瞬) を作った</title>
          <link>https://yukimemi.pages.dev/posts/shun/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/shun/</guid>
          <pubDate>Fri, 20 Mar 2026 12:00:00 GMT</pubDate>
          <description>Alfred や Raycast のようなキーボードドリブンなランチャー shun を Rust + Tauri + Svelte で作りました。</description>
          <content:encoded><![CDATA[<blockquote>
<p>[!NOTE] Update (2026-04-04)
shun の大幅なバージョンアップ（v5.0.1）に伴い、履歴スキーマ（バージョン 2）の変更や、新しい設定項目、プレビューパネルの追加などを反映しました。</p>
</blockquote>
<p>Alfred や Raycast が好きで、Windows でも同じような体験がしたくて、<a rel="external" href="https://claude.com/claude-code">Claude Code</a> と一緒にランチャーを作りました。</p>
<a href="https://github.com/yukimemi/shun" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/shun: A minimal keyboard-driven cross-platform launcher</div><div class="link-card-description">A minimal keyboard-driven cross-platform launcher. Contribute to yukimemi/shun development by creating an account on GitHub.</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/0d7a87813408daa9be59fe5945a914f51780e77dc000d81aefd0b0db453f1660/yukimemi/shun')"></div></a>
<h2 id="shun_とは">shun とは</h2>
<p><strong>shun (瞬)</strong> は、グローバルホットキーで呼び出せるキーボードドリブンのミニマルランチャーです。</p>
<ul>
<li>バックエンド: <strong>Rust + Tauri v2</strong></li>
<li>フロントエンド: <strong>Svelte 5</strong></li>
<li>検索エンジン: <strong>nucleo-matcher</strong>（Helix エディタと同じファジー検索エンジン）</li>
</ul>
<img src="https://github.com/yukimemi/shun/releases/download/v1.1.1/shun.gif" alt="shun demo" width="640" />
<h2 id="主な機能">主な機能</h2>
<h3 id="ファジー_/_完全一致_/_migemo_検索">ファジー / 完全一致 / migemo 検索</h3>
<p>nucleo-matcher によるファジー検索と完全一致検索を設定で切り替えられます。インストール済みアプリ、設定したアプリ、スキャンしたディレクトリのファイルが瞬時に絞り込まれます。</p>
<p><code>search_mode</code> には以下の5モードがあります。</p>
<table><thead><tr><th>モード</th><th>説明</th></tr></thead><tbody>
<tr><td><code>fuzzy</code></td><td>nucleo-matcher によるファジー検索（デフォルト）</td></tr>
<tr><td><code>exact</code></td><td>部分文字列による完全一致検索</td></tr>
<tr><td><code>migemo</code></td><td>ローマ字入力で日本語にマッチ</td></tr>
<tr><td><code>fuzzy_migemo</code></td><td>fuzzy と migemo の和集合（fuzzy 優先）</td></tr>
<tr><td><code>exact_migemo</code></td><td>exact と migemo の和集合（exact 優先）</td></tr>
</tbody></table>
<p>設定ファイルで固定するほか、<code>Ctrl+Shift+m</code> でその場でサイクル切り替えできます。ソート順（回数優先 / 最近優先）も <code>Ctrl+Shift+o</code> で切り替え可能です。切り替えた状態は <code>/save</code> コマンドで <code>config.local.toml</code> に永続化できます。</p>
<p><code>migemo</code> 系モードでは、ローマ字で日本語のファイル名や候補を検索できます。たとえば <code>hajime</code> と入力すると <code>初めてのRust</code> や <code>はじめに</code> にマッチします。<code>fuzzy_migemo</code> は「ローマ字でも日本語でもどちらでも引っかかる」ので、混在した候補リストを扱う場合に特に便利です。</p>
<p><a rel="external" href="https://github.com/oguna/rustmigemo">rustmigemo</a> と <a rel="external" href="https://github.com/oguna/jsmigemo">jsmigemo</a>（<a rel="external" href="https://github.com/oguna">oguna</a> 氏作）を使用。辞書（<a rel="external" href="https://github.com/oguna/yet-another-migemo-dict">yet-another-migemo-dict</a>、BSD-3-Clause）はアプリに同梱しているので別途インストール不要です。</p>
<h3 id="起動履歴_&amp;_よく使う順_/_最近使った順ソート">起動履歴 &amp; よく使う順 / 最近使った順ソート</h3>
<p>起動したアプリを自動的に記録し、実行回数と最終使用日時の組み合わせでソートされます。設定の <code>sort_order</code> でどちらを優先するか切り替え可能です。</p>
<ul>
<li><code>count_first</code>: 実行回数を優先し、同じ回数なら新しい順に表示。</li>
<li><code>recent_first</code>: 最終使用日時を優先し、同じ日時なら回数が多い順に表示。</li>
</ul>
<h3 id="Args_モード（Tab_キー）">Args モード（Tab キー）</h3>
<p>Tab キーを押すと Args モードに入り、追加の引数を渡して起動できます。バージョンアップにより、<strong>「どのアプリを、どの引数で実行したか」</strong> が個別に履歴として記憶されるようになりました。</p>
<pre><code>edge   →  Tab  →  https://bing.com     →  Enter
chrome →  Tab  →  https://google.co.jp →  Enter
</code></pre>
<p><img src="/static/images/shun-args-edge-bing.png" alt="Edge で Bing を開く" /></p>
<p><img src="/static/images/shun-args-chrome-google.png" alt="Chrome で Google を開く" /></p>
<p>ブラウザごとに開く URL を使い分けるのが特に便利です。一度使った組み合わせは引数込みで履歴に残るので、次回からは <code>edbi</code> と入力するだけで「Edge で Bing を開く」が候補に出てきます。</p>
<p>以前使った引数の組み合わせが履歴として記憶されていて、次回はゴーストテキストとして表示されます。<code>Ctrl+f</code> で1単語、<code>Ctrl+e</code> で行全体を確定できます。History アイテムで Tab を押すと、その引数を引き継いで Args モードに入れます。</p>
<h3 id="パス_&amp;_URL_を直接開く">パス &amp; URL を直接開く</h3>
<ul>
<li><code>~/Documents</code> や <code>/usr/bin</code> のような絶対パスを入力 → エクスプローラーや Finder で開く</li>
<li><code>%APPDATA%</code> や <code>$HOME</code>, <code>${XDG_CONFIG_HOME}</code> などの<strong>環境変数を含むパス</strong>を直接入力 → 展開して開く</li>
<li><code>shell:startup</code> などの <strong>Windows 特殊フォルダ</strong>を入力 → 直接開く</li>
<li><code>https://...</code> を入力 → ブラウザで開く</li>
<li>Tab で補完候補がドロップダウン表示</li>
</ul>
<h3 id="プレビューパネル">プレビューパネル</h3>
<p><code>Ctrl+Shift+p</code> でプレビューパネルを表示できます。</p>
<ul>
<li><strong>Args モード</strong>: ファイルパス補完中に、選択しているファイルの中身を表示。</li>
<li><strong>検索結果</strong>: 実行ファイルやスクリプト、テキストファイルなどの内容を表示。</li>
</ul>
<p>プレビューには <a rel="external" href="https://shiki.style/">Shiki</a> による構文強調（シンタックスハイライト）が適用され、テーマに合わせた色で見やすく表示されます。<code>Ctrl+j</code> / <code>Ctrl+k</code> でパネル内のスクロールも可能です。</p>
<h3 id="ウィンドウの移動">ウィンドウの移動</h3>
<p>ウィンドウ上部の <code>⠿</code> マーク（ドラッグハンドル）をマウスで掴んで、好きな位置にウィンドウを移動できます。移動した位置は <code>/save position</code> コマンドで <code>config.local.toml</code> に保存し、次回起動時にも維持することが可能です。</p>
<h3 id="スラッシュコマンド">スラッシュコマンド</h3>
<table><thead><tr><th>コマンド</th><th>動作</th></tr></thead><tbody>
<tr><td><code>/exit</code></td><td>アプリ終了</td></tr>
<tr><td><code>/config</code></td><td>設定ファイルを開く（Tab で <code>config.local.toml</code> など他のファイルを選択可）</td></tr>
<tr><td><code>/history</code></td><td>履歴ファイルを開く</td></tr>
<tr><td><code>/reload</code></td><td>設定を再読み込み（グローバルショートカット、アプリスキャン、各種設定を反映）</td></tr>
<tr><td><code>/update</code></td><td>アップデート確認・実行</td></tr>
<tr><td><code>/version</code></td><td>バージョン表示</td></tr>
<tr><td><code>/theme</code></td><td>テーマを即時切り替え（Tab でプリセットを選択可）</td></tr>
<tr><td><code>/save &lt;item&gt;</code></td><td>現在の search_mode / sort_order / theme / monitor / position / size を <code>config.local.toml</code> に保存（Tab で選択可）</td></tr>
<tr><td><code>/reset &lt;item&gt;</code></td><td><code>config.local.toml</code> に保存した上記設定をリセット（Tab で選択可）</td></tr>
<tr><td><code>/help</code></td><td>キーバインドと現在のステータスを確認できるヘルプパネルを表示</td></tr>
</tbody></table>
<p><code>/save</code> や <code>/config</code> などのコマンドは、Tab キーを押すことで引数の候補（設定項目やファイル名）がドロップダウン表示されます。</p>
<h3 id="その他の便利な操作">その他の便利な操作</h3>
<ul>
<li><strong>Shift + Enter</strong>: 履歴の候補を無視して、現在入力しているクエリを直接コマンドとして実行します。</li>
<li><strong>Ctrl + w / Ctrl + u</strong>: カーソル前の 1 単語削除、または行頭までの削除。</li>
<li><strong>Ctrl + d</strong>: 選択した履歴アイテム（特定の引数セットを含む）を削除。</li>
</ul>
<h3 id="アップデート検知">アップデート検知</h3>
<p>バックグラウンドで定期的に新バージョンをチェックします（デフォルト1時間ごと、設定変更可能）。新バージョンが見つかると、検索欄のプレースホルダーが <code>Update available: vX.X.X — /update</code> に変わって通知してくれます。</p>
<p>実際のアップデートは手動で、<code>/update</code> を実行すると起動します。ダウンロードの進捗がリアルタイムで表示され、完了後に自動再起動します。</p>
<p>Scoop や Homebrew でインストールした場合は、それぞれ <code>scoop update shun</code> / <code>brew upgrade --cask shun</code> を代わりに実行してくれます。</p>
<h2 id="インストール">インストール</h2>
<h3 id="ワンライナー">ワンライナー</h3>
<p><strong>Windows</strong> (PowerShell、管理者権限不要):</p>
<pre><code data-lang="powershell">irm https://yukimemi.github.io/shun/install.ps1 | iex
</code></pre>
<p><strong>macOS / Linux</strong>:</p>
<pre><code data-lang="bash">curl -fsSL https://yukimemi.github.io/shun/install.sh | sh
</code></pre>
<h3 id="パッケージマネージャー">パッケージマネージャー</h3>
<p><strong>WinGet (Windows)</strong>:</p>
<pre><code data-lang="powershell">winget install yukimemi.shun
</code></pre>
<p><strong>Scoop (Windows)</strong>:</p>
<pre><code data-lang="powershell">scoop bucket add yukimemi https://github.com/yukimemi/scoop-bucket
scoop install yukimemi/shun
</code></pre>
<p><strong>Homebrew (macOS)</strong>:</p>
<pre><code data-lang="bash">brew tap yukimemi/tap
brew install --cask yukimemi/tap/shun
</code></pre>
<h2 id="設定">設定</h2>
<p>初回起動時に自動生成されます。</p>
<table><thead><tr><th>OS</th><th>パス</th></tr></thead><tbody>
<tr><td>Windows</td><td><code>%APPDATA%\shun\config.toml</code></td></tr>
<tr><td>macOS</td><td><code>~/Library/Application Support/shun/config.toml</code></td></tr>
<tr><td>Linux</td><td><code>~/.config/shun/config.toml</code></td></tr>
</tbody></table>
<p>設定できる項目は多岐にわたります。詳細は <a rel="external" href="https://github.com/yukimemi/shun#configuration">README</a> を参照してください。ここでは代表的な例を紹介します。</p>
<pre><code data-lang="toml"># 検索モードとソート順
search_mode = &quot;fuzzy_migemo&quot;  # &quot;fuzzy&quot; | &quot;exact&quot; | &quot;migemo&quot; | &quot;fuzzy_migemo&quot; | &quot;exact_migemo&quot;
sort_order  = &quot;count_first&quot;   # &quot;count_first&quot; | &quot;recent_first&quot;
history_max_items = 1000      # 保持する履歴の最大数

# ウィンドウ・表示設定
max_items    = 8              # 候補の最大表示数
hide_on_blur = true           # フォーカスが外れたら自動的に閉じる
monitor      = &quot;cursor&quot;       # 起動モニター (&quot;cursor&quot; | &quot;primary&quot; | インデックス番号)
font_size    = 14             # フォントサイズ
opacity      = 1.0            # ウィンドウの不透明度 (0.0 - 1.0)
icon_style   = &quot;unicode&quot;      # ステータスバッジのスタイル (&quot;unicode&quot; | &quot;svg&quot;)

# プレビュー・その他
preview_args   = true         # 引数モードでのプレビュー (デフォルト: true)
preview_search = true         # 検索結果のプレビュー (デフォルト: true)
auto_start     = true         # ログイン時に自動起動 (デフォルト: true)

[keybindings]
launch      = &quot;Ctrl+Space&quot;    # グローバルホットキー
next        = &quot;Ctrl+n&quot;
prev        = &quot;Ctrl+p&quot;
confirm     = &quot;Enter&quot;
arg_mode    = &quot;Tab&quot;
accept_line = &quot;Ctrl+e&quot;        # ゴーストテキストを全確定
accept_word = &quot;Ctrl+f&quot;        # ゴーストテキストを 1 単語確定
delete_word = &quot;Ctrl+w&quot;        # カーソル前の 1 単語削除
delete_line = &quot;Ctrl+u&quot;        # 行頭まで削除
run_query   = &quot;Shift+Enter&quot;   # 履歴を無視して直接実行
delete_item       = &quot;Ctrl+d&quot;        # 選択した履歴アイテムを削除
cycle_search_mode = &quot;Ctrl+Shift+m&quot;  # 検索モードをサイクル切り替え
cycle_sort_order  = &quot;Ctrl+Shift+o&quot;  # ソート順をトグル
toggle_preview    = &quot;Ctrl+Shift+p&quot;  # プレビューパネルの表示/非表示
preview_scroll_down = &quot;Ctrl+j&quot;      # プレビューパネルをスクロール
preview_scroll_up   = &quot;Ctrl+k&quot;      # プレビューパネルをスクロール
close             = &quot;Escape&quot;

# 自動で見つかったアイテム（scan_dirs やシステムアプリ）の挙動を上書き
[[overrides]]
name       = &quot;scoop&quot;          # アプリ名（ファイル名）でマッチ
completion = &quot;list&quot;           # スキャンで見つかったコマンドに補完を追加できる
completion_list = [&quot;install&quot;, &quot;update&quot;, &quot;search&quot;, &quot;list&quot;, &quot;status&quot;, &quot;info&quot;]

[[overrides]]
ext        = &quot;xlsx&quot;           # 拡張子でマッチ
path       = &quot;C:/Program Files/Microsoft Office/root/Office16/EXCEL.EXE&quot;
args       = [&quot;{{ file_path }}&quot;] # 本来のパス（Excel ファイル自体のパス）を引数で渡す

# ファイルパス補完付きでエディタを開く
[[apps]]
name       = &quot;Neovide&quot;
path       = &quot;neovide&quot;
completion = &quot;path&quot;

# docker exec の補完をコマンド出力から生成
[[apps]]
name               = &quot;docker exec&quot;
path               = &quot;docker&quot;
args               = [&quot;exec&quot;, &quot;-it&quot;]
completion         = &quot;command&quot;
completion_command = &quot;docker ps --format &#39;{{.Names}}&#39;&quot;

# git checkout のブランチ補完
[[apps]]
name               = &quot;git checkout&quot;
path               = &quot;git&quot;
args               = [&quot;checkout&quot;]
completion         = &quot;command&quot;
completion_command = &quot;git branch --format=&#39;%(refname:short)&#39;&quot;
workdir            = &quot;~/src/myproject&quot;

# ディレクトリをまとめてスキャン
[[scan_dirs]]
path       = &quot;~/.local/bin&quot;
recursive  = false
extensions = [&quot;sh&quot;, &quot;py&quot;, &quot;ps1&quot;]
</code></pre>
<h3 id="テンプレートプレースホルダー">テンプレートプレースホルダー</h3>
<p><code>path</code> や <code>args</code> には <a rel="external" href="https://keats.github.io/tera/">Tera</a> テンプレート構文が使えます。Tab で Args モードに入って入力した内容が <code>{{ args }}</code>（文字列全体）や <code>{{ args_list }}</code>（スペース区切りの配列）に展開されます。</p>
<p>利用可能な変数・関数：</p>
<ul>
<li><code>{{ args }}</code>: 入力内容全体（文字列）</li>
<li><code>{{ args_list.0 }}</code>: スペース区切りの第1引数（0番目）</li>
<li><code>{{ env.VAR_NAME }}</code>: 環境変数</li>
<li><code>{{ vars.my_var }}</code>: <code>[vars]</code> セクションで定義した変数</li>
<li><code>{{ file_path }}</code>: <code>overrides</code> で差し替えられた元のファイルパス（フルパス）</li>
<li><code>{{ now() | date(format="%Y%m%d") }}</code>: 現在の日時</li>
<li><code>{{ args | urlencode }}</code>: URL エンコードフィルタ</li>
</ul>
<h4 id="Web_検索をランチャーから">Web 検索をランチャーから</h4>
<p><code>| urlencode</code> フィルタを使うと日本語もそのまま検索できます。</p>
<pre><code data-lang="toml"># Google 検索
[[apps]]
name = &quot;Google&quot;
path = &quot;https://www.google.com/search?q={{ args | urlencode }}&quot;

# 第1引数、第2引数を個別に使いたい場合
[[apps]]
name = &quot;Multi Search&quot;
path = &quot;https://example.com/search?q1={{ args_list.0 }}&amp;q2={{ args_list.1 }}&quot;

[[apps]]
name = &quot;Perplexity&quot;
path = &quot;https://www.perplexity.ai/search?q={{ args | urlencode }}&quot;

[[apps]]
name = &quot;GitHub Search&quot;
path = &quot;https://github.com/search?q={{ args | urlencode }}&quot;
</code></pre>
<p>使い方: <code>perp</code> → Tab → <code>rust の所有権とは</code> → Enter で Perplexity が開きます。一度使った検索クエリは履歴に残るので、次回は候補として出てきます。</p>
<h4 id="環境変数">環境変数</h4>
<p><code>{{ env.VAR_NAME }}</code> または Tera 標準の <code>{{ get_env(name="VAR") }}</code> で環境変数を参照できます。</p>
<pre><code data-lang="toml"># プロジェクトを引数で指定してエディタで開く（src/ 以下の相対パスで補完される）
[[apps]]
name = &quot;Open Project&quot;
path = &quot;neovide&quot;
args = [&quot;{{ env.USERPROFILE }}/src/{{ args }}&quot;]
completion = &quot;path&quot;

# default 指定（Tera 標準関数）
[[apps]]
name = &quot;Work Dir&quot;
path = &#39;{{ get_env(name=&quot;WORK_DIR&quot;, default=&quot;~/work&quot;) }}/{{ args }}&#39;
</code></pre>
<h4 id="日付プレフィックス付きメモ">日付プレフィックス付きメモ</h4>
<p><code>now()</code> 関数と <code>date</code> フィルターで今日の日付を埋め込めます（Tera 標準機能）。</p>
<pre><code data-lang="toml"># MemoNew: Tab → タイトル入力 → Enter で &quot;20260321-タイトル.md&quot; を作成して開く
[[apps]]
name       = &quot;MemoNew&quot;
path       = &quot;nvim&quot;
args       = [&#39;~/memo/{{ now() | date(format=&quot;%Y%m%d&quot;) }}-{{ args }}.md&#39;]
completion = &quot;none&quot;

# MemoList: Tab → ~/memo/ 以下のファイルを migemo パス補完で選択 → Enter
# &quot;hajime&quot; で &quot;初めて.md&quot; がヒット
[[apps]]
name                   = &quot;MemoList&quot;
path                   = &quot;nvim&quot;
args                   = [&quot;~/memo/{{ args }}&quot;]
completion             = &quot;path&quot;
completion_search_mode = &quot;migemo&quot;
</code></pre>
<p><code>date(format="%Y-%m-%d")</code> のようにフォーマットを変えるのも自由です。設定だけで完結する、ちょっとしたメモ管理フローが作れます。</p>
<h3 id="履歴ファイル">履歴ファイル</h3>
<p>履歴は以下のパスにシンプルな JSON として保存されています。バージョン 2 以降、構造が配列形式になり、各エントリが <code>key</code> と <code>args</code>（配列）を個別に持つようになりました。これにより、以前の形式（文字列結合）では失われがちだった「スペースを含む引数」なども正確（lossless）に保持・復元できるようになっています。</p>
<table><thead><tr><th>OS</th><th>パス</th></tr></thead><tbody>
<tr><td>Windows</td><td><code>%APPDATA%\shun\history.json</code></td></tr>
<tr><td>macOS</td><td><code>~/Library/Application Support/shun/history.json</code></td></tr>
<tr><td>Linux</td><td><code>~/.config/shun/history.json</code></td></tr>
</tbody></table>
<pre><code data-lang="json">{
  &quot;version&quot;: 2,
  &quot;entries&quot;: [
    {
      &quot;key&quot;: &quot;msedge.exe&quot;,
      &quot;args&quot;: [&quot;https://bing.com&quot;],
      &quot;count&quot;: 12,
      &quot;last_used&quot;: 1742478231
    },
    {
      &quot;key&quot;: &quot;Google&quot;,
      &quot;args&quot;: [&quot;shun&quot;, &quot;launcher&quot;],
      &quot;count&quot;: 5,
      &quot;last_used&quot;: 1775302627
    }
  ]
}
</code></pre>
<p>普通の JSON なので、テキストエディタで直接編集できます。<code>/history</code> コマンドで即座に開けます。</p>
<p>不要な履歴アイテムを1件ずつ消したいだけなら、ランチャー上で候補を選んで <code>Ctrl+d</code> を押すだけで削除できます。</p>
<h3 id="マシン固有の設定は_config.local.toml_に">マシン固有の設定は config.local.toml に</h3>
<p><code>config.toml</code> と同じディレクトリに <code>config.local.toml</code> を置くと、その内容がマージされます。<code>config.toml</code> を dotfiles などで複数マシン共通に管理しつつ、マシンごとに異なるスキャン対象パスや追加アプリは <code>config.local.toml</code> に書く、という運用が便利です。</p>
<pre><code data-lang="toml"># config.local.toml（このマシンだけのスキャン追加）
[[scan_dirs]]
path = &quot;C:/work/projects&quot;
recursive = true
extensions = [&quot;exe&quot;, &quot;bat&quot;, &quot;ps1&quot;]
</code></pre>
<h3 id="ユーザー定義変数_[vars]">ユーザー定義変数 [vars]</h3>
<p><code>[vars]</code> セクションに任意のキーを定義すると、<code>path</code> や <code>args</code> の Tera テンプレートから <code>{{ vars.キー名 }}</code> で参照できます。<code>config.toml</code> と <code>config.local.toml</code> はマージしてから展開されるので、<code>config.local.toml</code> 側の <code>[vars]</code> に書いた値を <code>config.toml</code> の <code>[[apps]]</code> で使うことができます。マシンごとに異なるパスなどを <code>config.local.toml</code> にだけ書いておく運用が便利です。</p>
<pre><code data-lang="toml"># config.local.toml
[vars]
work_dir = &quot;C:/work/myproject&quot;
editor   = &quot;neovide&quot;

# config.toml（config.local.toml の vars を参照できる）
[[apps]]
name = &quot;Open Work&quot;
path = &quot;{{ vars.editor }}&quot;
args = [&quot;{{ vars.work_dir }}&quot;]
</code></pre>
<h2 id="技術的なポイント">技術的なポイント</h2>
<h3 id="Rust_+_Tauri_v2">Rust + Tauri v2</h3>
<p>ネイティブアプリとして軽量・高速。バックエンドは Rust で書かれており、グローバルショートカットの登録、アプリスキャン、検索、履歴管理などをすべて Rust で処理しています。</p>
<h3 id="nucleo-matcher">nucleo-matcher</h3>
<p>Helix エディタが採用しているファジーマッチャーで、精度と速度のバランスが良いです。</p>
<h3 id="rustmigemo_/_jsmigemo">rustmigemo / jsmigemo</h3>
<p><a rel="external" href="https://github.com/oguna">oguna</a> 氏による Pure Rust / Pure JS の migemo 実装。辞書（<a rel="external" href="https://github.com/oguna/yet-another-migemo-dict">yet-another-migemo-dict</a>、Mozc + UniDic ベースの BSD-3-Clause ライセンス）をバイナリに同梱し、インストール不要で日本語ローマ字検索を実現しています。</p>
<h3 id="Svelte_5_(Runes)">Svelte 5 (Runes)</h3>
<p>フロントエンドは Svelte 5 の Runes (<code>$state</code>, <code>$derived</code>, <code>$effect</code>) で記述。シンプルな UI ながら補完ドロップダウンやゴーストテキストなど複数の状態を管理しています。</p>
<h2 id="おわりに">おわりに</h2>
<p>このランチャーは <a rel="external" href="https://claude.com/claude-code">Claude Code</a> と一緒に開発しました。自分でアーキテクチャや機能の方向性を決めつつ、Rust の細かい実装や CI/CD の設定などは Claude Code に大きく助けてもらっています。「こういう動きにしたい」という意図を伝えるだけでコードに落としてくれるので、アイデアを素早く形にできました。</p>
<p>Tauri v2 を使っているので Windows / macOS / Linux すべてでビルド・リリースしていますが、動作確認は Windows のみで行っています。macOS・Linux で試してみた方がいれば、フィードバックいただけると嬉しいです！</p>
<p>ぜひ試してみてください！</p>
<a href="https://github.com/yukimemi/shun" class="link-card"><div class="link-card-content"><div class="link-card-title">GitHub - yukimemi/shun: A minimal keyboard-driven cross-platform launcher</div><div class="link-card-description">A minimal keyboard-driven cross-platform launcher. Contribute to yukimemi/shun development by creating an account on GitHub.</div><div class="link-card-meta"><img src="https://www.google.com/s2/favicons?domain=github.com" class="link-card-favicon"><span>github.com</span></div></div><div class="link-card-image" style="background-image: url('https://opengraph.githubassets.com/0d7a87813408daa9be59fe5945a914f51780e77dc000d81aefd0b0db453f1660/yukimemi/shun')"></div></a>
]]></content:encoded>
      </item>
      <item>
          <title>Neovim ターミナルの PowerShell で実現する「二段階 Esc」と視覚的モード管理</title>
          <link>https://yukimemi.pages.dev/posts/neovim-terminal-double-esc/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/neovim-terminal-double-esc/</guid>
          <pubDate>Sun, 01 Mar 2026 21:30:00 GMT</pubDate>
          <description>Neovim の :terminal でシェルの vi キーバインドを快適に使うための、PowerShell 側の Esc 制御と Neovim 側の視覚的モード管理ハックを紹介します。</description>
          <content:encoded><![CDATA[<p>Neovim の <code>:terminal</code> でシェル（PowerShell）を操作している時、シェルの <code>vi</code> キーバインドを有効にしていると「Esc キーの奪い合い」が発生します。</p>
<ol>
<li>シェルの挿入モードからノーマルモードに戻りたい（シェルの操作）。</li>
<li>Neovim 自体のノーマルモードに戻って、ウィンドウ移動やバッファ切り替えをしたい（Neovim の操作）。</li>
</ol>
<p>これをスムーズに行うための「二段階 Esc」設定が非常に快適だったので紹介します。</p>
<h2 id="課題：Esc_一発で_Neovim_モードに戻ってしまう問題">課題：Esc 一発で Neovim モードに戻ってしまう問題</h2>
<p>通常、ターミナルモードで <code>Esc</code> を Neovim の <code>&lt;C-\&gt;&lt;C-n&gt;</code>（モード抜け）にマッピングしてしまうと、シェルの <code>vi</code> モードとしての <code>Esc</code> が効かなくなります。これではシェルのコマンド履歴検索や行編集の恩恵を受けられません。</p>
<p>かといって、シェルの <code>vi</code> モードを活かすためにマッピングを外すと、Neovim 自体の操作に移るために毎回 <code>&lt;C-\&gt;&lt;C-n&gt;</code> を打つ必要があり、これが地味にストレスです。</p>
<h2 id="解決策：PowerShell_側での「二段階_Esc」実装">解決策：PowerShell 側での「二段階 Esc」実装</h2>
<p>この問題は、PowerShell の <code>PSReadLine</code> で「現在のモード」を判定して <code>Esc</code> の挙動を変えることで解決できました。</p>
<p>具体的には、PowerShell のプロファイル（$PROFILE）に以下のような設定を追加しています。</p>
<pre><code data-lang="powershell"># PSReadLine の ViMode を利用
if ($env:NVIM) {
  # シェルがすでに Command (Normal) モードの時に Esc を押すと、
  # Neovim 本体のサーバに対してモード変更のリモート命令を送る
  Set-PSReadLineKeyHandler -Chord Escape -ViMode Command -ScriptBlock {
    &amp; nvim --server $env:NVIM --remote-send &quot;&lt;C-\&gt;&lt;C-n&gt;&quot;
  }
}
</code></pre>
<p>この設定により、以下のような流れるような操作が可能になります。</p>
<ul>
<li><strong>1回目の Esc</strong>: シェルが <code>Insert</code> → <code>Command</code> モードに移行。シェルの <code>vi</code> 操作が可能になる。</li>
<li><strong>2回目の Esc</strong>: シェルがすでに <code>Command</code> モードなら、Neovim 本体が <code>Terminal-Normal</code> モードに移行。</li>
</ul>
<p>今回は PowerShell での設定を紹介しましたが、<code>nvim --server</code> を利用したリモート送信自体は Neovim の標準的な機能です。そのため、zsh や bash など、他のシェルでも <code>bindkey</code> などを利用して「ノーマルモード中に Esc を押した時の挙動」をカスタマイズすれば、同様の「二段階 Esc」が実現可能と思われます。</p>
<h2 id="視覚的なモード管理（Neovim_側）">視覚的なモード管理（Neovim 側）</h2>
<p>二段階に分けたことで、「今どちらのノーマルモード（シェル or Neovim）にいるのか」を判別する必要があります。これを Neovim 側の <code>autocmd</code> で視覚的にサポートします。</p>
<p><img src="/static/images/neovim-terminal-double-esc.gif" alt="Neovim terminal double esc" /></p>
<pre><code data-lang="lua">-- Terminal-Normal モード（Neovim 制御下）になった時だけ枠線を赤く光らせる
vim.api.nvim_create_autocmd(&quot;TermLeave&quot;, {
  callback = function()
    if vim.bo.buftype == &quot;terminal&quot; then
      -- モードを抜けた（Terminal-Normal になった）時に ErrorMsg ハイライトを適用
      vim.wo.winhighlight = &quot;FloatBorder:ErrorMsg&quot;
    end
  end,
})

vim.api.nvim_create_autocmd(&quot;TermEnter&quot;, {
  callback = function()
    if vim.bo.buftype == &quot;terminal&quot; then
      -- 再びターミナル操作に戻った時にハイライトをクリア
      vim.wo.winhighlight = &quot;&quot;
    end
  end,
})
</code></pre>
<p>この設定（ここでは浮動ウィンドウの <code>FloatBorder</code> を想定）により、<strong>「枠線が赤く光っていれば Neovim 操作モード」「そうでなければシェル操作モード」</strong> と一目で判別できるようになります。</p>
<p>※ハイライトグループや適用先（<code>FloatBorder</code> や <code>StatusLine</code> など）は、自身のターミナルの開き方や好みに合わせて適宜調整してください。</p>
<h2 id="まとめ">まとめ</h2>
<p>この「二段階 Esc」と「視覚的なフィードバック」を組み合わせることで、ターミナル内での <code>vi</code> キーバインドの利便性を最大化しつつ、Neovim 本体の操作への復帰もストレスなく行えるようになりました。</p>
<p>ターミナル作業が多い Vimmer の方、特に PowerShell をメインに使っている方には非常におすすめのセットアップです。</p>
<h2 id="参考設定（dotfiles）">参考設定（dotfiles）</h2>
<p>実際の私の設定は、以下のリポジトリで公開しています。</p>
<h3 id="PowerShell_(PSReadLine_の_Esc_制御)">PowerShell (PSReadLine の Esc 制御)</h3>
<div class="remote-code-container"><div class="remote-code-header" data-pagefind-ignore><a href="https://github.com/yukimemi/dotfiles/blob/517a49b5b0ce1472ad66a77af9f7df2f90233591/dot_config/powershell/LazyProfile.psm1#L339-L341">dot_config/powershell/LazyProfile.psm1 (L339-L341)</a></div><div class="remote-code-body"><div class="code-content"><pre><code class="language-powershell highlight">  if ($env:NVIM) {
    Set-PSReadLineKeyHandler -Chord Escape -ViMode Command -ScriptBlock { &amp; nvim --server $env:NVIM --remote-send "&lt;C-\&gt;&lt;C-n&gt;" }
  }</code></pre></div></div></div>
<h3 id="Neovim_(autocmd_による枠線の可視化)">Neovim (autocmd による枠線の可視化)</h3>
<div class="remote-code-container"><div class="remote-code-header" data-pagefind-ignore><a href="https://github.com/yukimemi/dotfiles/blob/517a49b5b0ce1472ad66a77af9f7df2f90233591/dot_config/nvim/rc/after/toggleterm.lua#L54-L72">dot_config/nvim/rc/after/toggleterm.lua (L54-L72)</a></div><div class="remote-code-body"><div class="code-content"><pre><code class="language-lua highlight">vim.api.nvim_create_autocmd("TermLeave", {
  callback = function()
    if vim.bo.buftype == "terminal" then
      vim.wo.winhighlight = "FloatBorder:ErrorMsg"
    end
  end,
})

vim.api.nvim_create_autocmd("TermEnter", {
  callback = function()
    if vim.bo.buftype == "terminal" then
      vim.wo.winhighlight = ""
    end
  end,
})
</code></pre></div></div></div>
]]></content:encoded>
      </item>
      <item>
          <title>OpenClaw を Discord で動かす</title>
          <link>https://yukimemi.pages.dev/posts/openclaw-discord-setup/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/openclaw-discord-setup/</guid>
          <pubDate>Sun, 01 Feb 2026 17:50:00 GMT</pubDate>
          <description>OpenClaw を Discord で動かすためのセットアップ手順まとめ。</description>
          <content:encoded><![CDATA[<p><a rel="external" href="https://openclaw.ai/">OpenClaw</a> を Discord で動かすためのセットアップを行いました。
設定ファイルや AI の記憶（ワークスペース）を安全に管理しつつ、快適な AI エージェント環境を構築する手順をまとめます。</p>
<h2 id="OpenClaw_とは">OpenClaw とは</h2>
<p>OpenClaw は、自律的な AI エージェントをローカルで動かし、WhatsApp や Discord などのメッセージングアプリと連携させることができるツールです。
シェルコマンドの実行やファイル操作などのスキルを持たせることができ、AI が「思考」するだけでなく「実行」まで担ってくれます。</p>
<h2 id="セットアップ手順">セットアップ手順</h2>
<h3 id="1._Discord_Bot_の作成">1. Discord Bot の作成</h3>
<p>まずは Discord 側で Bot アカウントを作成します。</p>
<ol>
<li><a rel="external" href="https://discord.com/developers/applications">Discord Developer Portal</a> で <code>New Application</code> を作成。</li>
<li><code>Bot</code> セクションでトークンを取得。</li>
<li><code>Privileged Gateway Intents</code> ですべての項目（特に <code>Message Content Intent</code>）を ON にして保存。</li>
<li><code>OAuth2</code> -&gt; <code>URL Generator</code> で <code>bot</code> スコープと <code>Administrator</code> 権限を選択し、生成された URL をブラウザに貼り付け、自分のサーバーに Bot を招待。</li>
</ol>
<p><strong>注意:</strong> Intents を変更した後は、一度サーバーから Bot をキックして招待し直さないと権限変更が反映されない場合があるので注意が必要です。</p>
<h3 id="2._OpenClaw_の初期化">2. OpenClaw の初期化</h3>
<pre><code data-lang="bash">mkdir ~/.openclaw
cd ~/.openclaw
npx openclaw onboard
</code></pre>
<p><code>onboard</code> コマンドを実行すると、対話形式でセットアップが始まります。
主な質問と設定内容は以下の通りです。</p>
<ol>
<li><strong>Risk Acknowledgement</strong>: リスクについての確認。<code>y</code> で承諾。</li>
<li><strong>Onboarding mode</strong>: <code>QuickStart</code> を選択。</li>
<li><strong>Model/auth provider</strong>: 使用する AI モデルを選択。Google (Gemini) や OpenAI などから選べます。
<ul>
<li>API キーの入力や、OAuth によるブラウザ認証（<code>Google Antigravity OAuth</code> など）を行います。</li>
</ul>
</li>
<li><strong>Channel Setup</strong>: ここで <code>Discord</code> を選択。
<ul>
<li>取得した <strong>Discord Bot Token</strong> を入力します。</li>
</ul>
</li>
<li><strong>Skills/Hooks</strong>: 依存関係（スキル）のインストールに使用するパッケージマネージャ（Bunなど）の選択や、自動化のための Hooks（<code>session-memory</code>, <code>command-logger</code> など）を有効にするかを選択します。</li>
</ol>
<p>設定が完了すると Gateway が起動し、Discord Bot がオンラインになります。</p>
<h3 id="3._systemd_による自動起動">3. systemd による自動起動</h3>
<p>セットアップの最後で、OpenClaw が「gateway サービスを systemd としてインストールし、有効化するか？」と聞いてくるので、<code>Yes</code> と答えるだけで自動的にバックグラウンドサービスとして常駐します。</p>
<h2 id="AI_の記憶と設定ファイルについて">AI の記憶と設定ファイルについて</h2>
<p>OpenClaw の最大の特徴は、AI の記憶や設定がすべてローカルの Markdown ファイルとして管理されている点です。
<code>~/.openclaw/workspace</code> ディレクトリには以下のようなファイルが生成されます。</p>
<ul>
<li><strong><code>IDENTITY.md</code></strong>: AI 自身のアイデンティティ（名前、性格、口調など）。ここを編集すると AI のキャラ付けを変更できます。</li>
<li><strong><code>USER.md</code></strong>: ユーザー（自分）に関する情報。名前や好みなどが記録されます。</li>
<li><strong><code>MEMORY.md</code></strong>: 長期記憶。重要な事実や文脈がここに蓄積されます。</li>
<li><strong><code>SOUL.md</code></strong>: AI の行動原理や「魂」にあたる部分。
<ul>
<li>このファイルは非常にユニークで、対話を通じて「実行前には必ず確認してほしい」とか「定期的にリポジトリを同期して」といったルールが追記されていきます。<code>git log</code> で履歴を見ると、AI が指導を受けて成長していく様子が分かります。</li>
</ul>
</li>
<li><strong><code>AGENTS.md</code></strong>, <strong><code>TOOLS.md</code></strong>: エージェントの構成や使用可能なツールの定義。</li>
</ul>
<p>Discord で会話しながら「私の名前は yukimemi だよ」と教えたり、「〜については覚えておいて」と頼んだりすると、AI が自律的にこれらのファイルを更新します。
記憶がテキストファイルとして可視化され、さらに Git でバージョン管理できるため、「いつ何を覚えたか」が履歴として残るのが非常に面白いです。</p>
<h2 id="遭遇したトラブルと解決策">遭遇したトラブルと解決策</h2>
<h3 id="Failed_to_resolve_Discord_application_id_エラー"><code>Failed to resolve Discord application id</code> エラー</h3>
<p>Discord Bot のトークンからアプリケーション ID が正しく解決できない場合に発生します。
これは設定ファイル (<code>openclaw.json</code>) の不整合が原因でしたが、<code>npx openclaw doctor --fix</code> を実行することで、設定ファイルを自動的に修復して解決できました。</p>
<h3 id="記憶（workspace）の管理">記憶（workspace）の管理</h3>
<p>前述の通り、OpenClaw のワークスペース（<code>~/.openclaw/workspace</code>）はそれ自体が Git リポジトリとして管理されています。
そのため、自分の <code>dotfiles</code> リポジトリなどでまとめて管理しようとするとサブモジュール扱いになってしまい、中身（AI の記憶）が保存されません。</p>
<p>今回はワークスペースを <code>dotfiles</code> の管理対象外（<code>.gitignore</code>）とし、<strong>エージェント自身に自分の記憶を Git 管理させる</strong> という運用スタイルにしました。
AI に「記憶をコミットして GitHub にプッシュしておいて」と頼むと、自分で <code>git commit</code> して、GitHub CLI (<code>gh</code>) を使ってリポジトリ作成からプッシュまで完遂してくれました。これは感動的です。</p>
<h2 id="まとめ">まとめ</h2>
<p>これで、Discord を通じていつでも自律エージェントと対話できる環境が整いました。
自分だけの AI アシスタント、これから育てていくのが楽しみです。</p>
]]></content:encoded>
      </item>
      <item>
          <title>PowerShell で abbr 展開</title>
          <link>https://yukimemi.pages.dev/posts/powershell-abbr/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/powershell-abbr/</guid>
          <pubDate>Sun, 18 Jan 2026 10:19:22 GMT</pubDate>
          <description>PowerShell で fish shell の abbr (abbreviation) のような機能を実装する方法。</description>
          <content:encoded><![CDATA[<p>PowerShell の <code>alias</code> は便利ですが、 fish shell の <code>abbr</code> や、 <code>zeno.zsh</code> のように入力した瞬間に展開される方が、履歴の可読性や実行コマンドの明示性の面でメリットがあります。</p>
<p>これを実現するために、<code>PSReadLine</code> のキーハンドラ機能を使って実装してみました。</p>
<h2 id="実装方法">実装方法</h2>
<p><code>$PROFILE</code> や、そこから読み込まれるモジュールファイルに以下のコードを追加します。</p>
<pre><code data-lang="powershell">  # --- Abbreviation Expansion ---
  $abbrs = @{
    &quot;b&quot;     = &quot;cd ..&quot;
    &quot;g&quot;     = &quot;git&quot;
    &quot;s&quot;     = &quot;jj status&quot;
    &quot;d&quot;     = &quot;jj diff&quot;
    &quot;o&quot;     = &quot;Start-Process&quot;
    &quot;a&quot;     = &quot;git add&quot;
    &quot;t&quot;     = &quot;exit&quot;
    &quot;which&quot; = &quot;Get-Command&quot;
    &quot;l&quot;     = &quot;Get-ChildItem&quot;
    &quot;la&quot;    = &quot;Get-ChildItem -Force&quot;
    &quot;c&quot;     = &quot;Clear-Host&quot;
    &quot;e&quot;     = &quot;nvim&quot;
  }

  $expandAbbrLogic = {
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

    if ($cursor -gt 0) {
      $sub = $line.Substring(0, $cursor)
      if ($sub -match &#39;(?&lt;Word&gt;\S+)$&#39;) {
        $word = $Matches[&#39;Word&#39;]
        if ($abbrs.ContainsKey($word)) {
          [Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor - $word.Length, $word.Length)
          [Microsoft.PowerShell.PSConsoleReadLine]::Insert($abbrs[$word])
        }
      }
    }
  }

  Set-PSReadLineKeyHandler -Key Spacebar -ViMode Insert -ScriptBlock {
    . $expandAbbrLogic
    [Microsoft.PowerShell.PSConsoleReadLine]::Insert(&#39; &#39;)
  }.GetNewClosure()

  Set-PSReadLineKeyHandler -Key Enter -ViMode Insert -ScriptBlock {
    . $expandAbbrLogic
    [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
  }.GetNewClosure()
</code></pre>
<h2 id="解説">解説</h2>
<h3 id="略語辞書の定義">略語辞書の定義</h3>
<p><code>$abbrs</code> ハッシュテーブルに、略語と展開後のコマンドを定義しています。
例えば <code>"g" = "git"</code> と定義しておくと、<code>g</code> と入力してスペースを押すと <code>git</code> に置き換わります。</p>
<p>自分は一文字コマンドをけっこうたくさん定義しています。
一文字でできるとすごい早くて便利！</p>
<h3 id="展開ロジック">展開ロジック</h3>
<p><code>$expandAbbrLogic</code> スクリプトブロックで実際の置換処理を行っています。</p>
<ol>
<li><code>GetBufferState</code> で現在の入力行とカーソル位置を取得。</li>
<li>カーソル直前の単語を正規表現 <code>(?&lt;Word&gt;\S+)$</code> で抽出。</li>
<li>その単語が <code>$abbrs</code> に存在すれば、<code>Delete</code> で削除し <code>Insert</code> で展開後の文字列を挿入。</li>
</ol>
<h3 id="キーハンドラの設定">キーハンドラの設定</h3>
<p><code>Set-PSReadLineKeyHandler</code> を使い、<code>Spacebar</code> (スペースキー) と <code>Enter</code> キーをフックしています。</p>
<ul>
<li><strong>Spacebar</strong>: 略語を展開してから、スペースを挿入します。これにより、<code>g</code> -&gt; Space -&gt; <code>git </code> となり、続けて引数を入力できます。</li>
<li><strong>Enter</strong>: コマンドの末尾で略語を使って即実行する場合（例: <code>exit</code> を <code>t</code> に割り当てて <code>t</code> -&gt; Enter）のために、Enter キーでも展開処理を走らせてから <code>AcceptLine</code> (実行) しています。</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>エイリアスとは異なり、実際に実行されるコマンドが入力中に明示され、そのまま履歴に残るのがメリットです。
コマンドの打ち間違いにも気づきやすくなるため、よく使うコマンドを登録しておくと重宝します。</p>
]]></content:encoded>
      </item>
      <item>
          <title>dvpm (Denops Vim Plugin Manager) をもっと便利に (キャッシュと遅延ロード)</title>
          <link>https://yukimemi.pages.dev/posts/dvpm_lazy_cache/</link><guid isPermaLink="false">https://yukimemi.pages.dev/posts/dvpm_lazy_cache/</guid>
          <pubDate>Mon, 12 Jan 2026 09:08:36 GMT</pubDate>
          <description>自作の plugin manager である dvpm に、キャッシュ機能と遅延ロード機能を追加して爆速かつ便利にした話</description>
          <content:encoded><![CDATA[<p><a rel="external" href="https://zenn.dev/yukimemi/articles/2023-06-09-dvpm">以前の記事</a> で自作のプラグインマネージャー <code>dvpm</code> を紹介してから、はや2年半以上が経過しました。
自分でも使い続けながら、少しずつ改良を重ねてきたのですが、特筆すべき大きな進化が2つあるので紹介したいと思います。</p>
<p>それが、<strong>キャッシュ機能</strong> と <strong>遅延ロード機能</strong> です。</p>
<span id="continue-reading"></span>
<h2 id="起動直後からプラグインを使いたい！_(キャッシュ機能)">起動直後からプラグインを使いたい！ (キャッシュ機能)</h2>
<p>前回の記事で、 <code>dvpm</code> の特徴として「どれだけプラグインを入れても起動速度が劣化しない」という点を挙げました。
しかし、これには「起動直後はプラグインがまだ読み込まれておらず、バックグラウンドで denops が立ち上がってから順次読み込まれる」というトレードオフがありました。</p>
<p>「爆速で起動してほしいけど、起動した直後から Telescope などのプラグインをすぐに使いたい……」</p>
<p>そんなわがままを叶えるのが <strong>キャッシュ機能</strong> です。</p>
<h3 id="仕組み">仕組み</h3>
<p><code>dvpm</code> のキャッシュ機能は、各プラグインの <code>runtimepath</code> への追加処理や、 <code>before</code> / <code>after</code> で指定した Vim script / Lua の設定を、一つの <code>.vim</code> ファイルに書き出します。</p>
<p>次回以降の Vim / Neovim 起動時に、このキャッシュファイルを <code>source</code> するように設定しておけば、 <strong>denops の起動を待たずして、通常のプラグインマネージャーと同じように起動時からプラグインが有効になります。</strong></p>
<h3 id="設定方法">設定方法</h3>
<p><code>Dvpm.begin</code> でキャッシュファイルの出力先を指定し、各プラグインで <code>cache.enabled: true</code> を設定するだけです。</p>
<pre><code data-lang="typescript">export async function main(denops: Denops): Promise&lt;void&gt; {
  const base_path = (await fn.has(denops, &quot;nvim&quot;)) ? &quot;~/.cache/nvim/dvpm&quot; : &quot;~/.cache/vim/dvpm&quot;;
  const base = (await fn.expand(denops, base_path)) as string;

  // キャッシュファイルの出力先を指定
  const cache_path = (await fn.has(denops, &quot;nvim&quot;))
    ? &quot;~/.config/nvim/plugin/dvpm_plugin_cache.vim&quot;
    : &quot;~/.config/vim/plugin/dvpm_plugin_cache.vim&quot;;
  const cache = (await fn.expand(denops, cache_path)) as string;

  const dvpm = await Dvpm.begin(denops, { base, cache });

  // キャッシュを有効にする
  await dvpm.add({
    url: &quot;tani/vim-artemis&quot;,
    cache: { enabled: true },
  });

  // 起動時にすぐ表示されてほしいダッシュボードなどはキャッシュに最適
  await dvpm.add({
    url: &quot;goolord/alpha-nvim&quot;,
    enabled: async ({ denops }) =&gt; await fn.has(denops, &quot;nvim&quot;),
    cache: {
      after: `
        lua &lt;&lt; EOB
          require(&quot;alpha&quot;).setup(require(&quot;alpha.themes.startify&quot;).config)
        EOB
      `,
    },
  });

  await dvpm.end();
}
</code></pre>
<div class="message"><p><strong>注意: キャッシュファイルのパスについて</strong></p>
<p><code>Dvpm.begin</code> に指定する <code>cache</code> のパスは、通常 <code>plugin/</code> ディレクトリの下など、 <strong>Vim / Neovim の <code>runtimepath</code> にデフォルトで含まれている場所</strong> を指定してください。
もし <code>runtimepath</code> 外の場所を指定した場合は、そのファイルを <code>source</code> するか、ディレクトリを <code>runtimepath</code> に追加する設定を自前で書く必要があります。</p>
</div>
<p>このように設定しておくと、 <code>dvpm</code> がキャッシュファイルを自動生成してくれます。
ダッシュボードのような「起動した瞬間に見えていてほしい」プラグインでも、ストレスなく利用できるようになります。</p>
<h3 id="キャッシュの詳細オプション">キャッシュの詳細オプション</h3>
<p>キャッシュ機能にも、通常ロードと同様に、以下の設定オプションが用意されています。</p>
<ul>
<li><code>before</code>: プラグインが <code>runtimepath</code> に追加される <strong>前</strong> に実行する Vim script / Lua を記述します。</li>
<li><code>after</code>: プラグインが <code>runtimepath</code> に追加された <strong>後</strong> に実行する Vim script / Lua を記述します。</li>
<li><code>beforeFile</code>: <code>before</code> と同様ですが、文字列ではなく外部ファイル（ <code>.vim</code> や <code>.lua</code> ）のパスを指定して読み込ませます。</li>
<li><code>afterFile</code>: <code>after</code> と同様に、外部ファイルのパスを指定します。</li>
</ul>
<p><strong><code>before</code>, <code>after</code>, <code>beforeFile</code>, <code>afterFile</code> のいずれかが設定されている場合、 <code>cache.enabled: true</code> は省略できます</strong></p>
<h2 id="必要な時だけ読み込みたい！_(遅延ロード機能)">必要な時だけ読み込みたい！ (遅延ロード機能)</h2>
<p>最近のプラグインマネージャー（ <code>lazy.nvim</code> など）では当たり前となっている <strong>遅延ロード (Lazy Load)</strong> にも対応しました。</p>
<p>「特定のファイルタイプを開いた時だけ」「特定のコマンドを叩いた時だけ」といった条件でプラグインをロードできます。</p>
<h3 id="設定例">設定例</h3>
<p><code>dvpm.add</code> の <code>lazy</code> オプションで指定します。</p>
<pre><code data-lang="typescript">  // コマンド実行時にロード
  await dvpm.add({
    url: &quot;tweekmonster/startuptime.vim&quot;,
    lazy: { cmd: &quot;StartupTime&quot; },
  });

  // 特定のイベント発生時にロード (例: インサートモードに入った時)
  await dvpm.add({
    url: &quot;cohama/lexima.vim&quot;,
    lazy: { event: &quot;InsertEnter&quot; },
  });

  // ファイルタイプに応じてロード
  await dvpm.add({
    url: &quot;OXY2DEV/markview.nvim&quot;,
    lazy: { ft: &quot;markdown&quot; },
    dependencies: [&quot;nvim-treesitter/nvim-treesitter&quot;],
  });

  // キー入力でロード
  await dvpm.add({
    url: &quot;mbbill/undotree&quot;,
    lazy: {
      keys: {
        lhs: &quot;&lt;leader&gt;u&quot;,
        rhs: &quot;&lt;cmd&gt;UndotreeToggle&lt;cr&gt;&quot;,
        mode: &quot;n&quot;,
        desc: &quot;Undo Tree&quot;,
      },
    },
  });

  // ライブラリとそれを使用するプラグインの遅延ロード
  await dvpm.add({
    url: &quot;kana/vim-textobj-user&quot;,
    lazy: { enabled: true },
  });
  await dvpm.add({
    url: &quot;kana/vim-textobj-entire&quot;,
    dependencies: [&quot;kana/vim-textobj-user&quot;],
    lazy: {
      keys: [
        { lhs: &quot;ie&quot;, mode: [&quot;x&quot;, &quot;o&quot;] },
        { lhs: &quot;ae&quot;, mode: [&quot;x&quot;, &quot;o&quot;] },
      ],
    },
  });
</code></pre>
<p><code>keys</code> の指定では、プロキシマッピングを自動で作成し、キーが押された瞬間にプラグインをロードして、本来の機能を実行するようになっています。 <code>mode</code> や <code>desc</code> を指定することで、 <code>which-key.nvim</code> などのプラグインとも相性良く設定できます。</p>
<h3 id="遅延ロードの詳細オプション">遅延ロードの詳細オプション</h3>
<p>遅延ロード設定は配列指定など、いくつかのパターンで設定できます。</p>
<ul>
<li><strong>配列での指定</strong>: <code>cmd</code>, <code>event</code>, <code>ft</code>, <code>keys</code> はいずれも配列を受け取れます。複数のコマンドやファイルタイプをきっかけにロードしたい場合に便利です。</li>
<li><strong><code>keys</code> の <code>lhs</code> のみ指定</strong>: <code>rhs</code> を省略して <code>lhs</code> だけを指定することも可能です。この場合、ロード後に <code>dvpm</code> が作成した暫定的なマッピングを解除（unmap）し、プラグイン側が本来定義しているマッピングが有効になるように振る舞います。</li>
<li><strong>依存関係の自動ロード</strong>: <code>lazy</code> 設定されたプラグインが読み込まれる際、 <code>dependencies</code> に指定されたプラグインも（それらが <code>lazy</code>であっても）自動的にロードされます。</li>
</ul>
<h3 id="ライフサイクルと_User_イベント">ライフサイクルと User イベント</h3>
<p><code>dvpm</code> はプラグインの登録からロードまでの各工程で、詳細に制御するためのフックと <code>User</code> イベントを提供しています。
フックの実行順序は以下の通りです。</p>
<ol>
<li><strong><code>add</code> / <code>addFile</code></strong>: <code>Dvpm.end()</code> 実行時に常に実行（ロードの有無に関わらず実行）。</li>
<li><strong><code>before</code> / <code>beforeFile</code></strong>: <code>runtimepath</code> に追加される直前に実行（遅延ロード時はロード発生時）。</li>
<li><strong><code>after</code> / <code>afterFile</code></strong>: <code>runtimepath</code> に追加され、 <code>plugin/*.vim</code> や <code>plugin/*.lua</code> がソースされた直後に実行。</li>
</ol>
<p>また、これらに合わせて詳細な <code>User</code> イベントも発火します。</p>
<h4 id="システムライフサイクルイベント">システムライフサイクルイベント</h4>
<ul>
<li><code>DvpmBeginPre</code> / <code>DvpmBeginPost</code>: <code>Dvpm.begin()</code> の前後。</li>
<li><code>DvpmEndPre</code> / <code>DvpmEndPost</code>: <code>Dvpm.end()</code> の前後。</li>
<li><code>DvpmInstallPre</code> / <code>DvpmInstallPost</code>: プラグイン全体のインストール前後。</li>
<li><code>DvpmUpdatePre</code> / <code>DvpmUpdatePost</code>: プラグイン全体のアップデート前後。</li>
<li><code>DvpmCacheUpdated</code>: キャッシュファイルが更新された時。</li>
</ul>
<h4 id="プラグイン個別イベント">プラグイン個別イベント</h4>
<ul>
<li><code>DvpmPluginLoadPre:{pluginName}</code> / <code>DvpmPluginLoadPost:{pluginName}</code>: 個別ロードの前後。</li>
<li><code>DvpmPluginInstallPre:{pluginName}</code> / <code>DvpmPluginInstallPost:{pluginName}</code>: 個別インストールの前後。</li>
<li><code>DvpmPluginUpdatePre:{pluginName}</code> / <code>DvpmPluginUpdatePost:{pluginName}</code>: 個別アップデートの前後。</li>
</ul>
<p>※ <code>{pluginName}</code> は <code>dvpm</code> で管理されているプラグイン名です。通常はリポジトリ URL の末尾（例: <code>cohama/lexima.vim</code> なら <code>lexima.vim</code>）になりますが、 <code>dvpm.add</code> の際に <code>name</code> プロパティで任意の名前を指定することも可能です。</p>
<p>このように、Vim script / Lua 側からも <code>autocmd User DvpmPluginLoadPost:lexima.vim ...</code> といった形で、特定のプラグインがロードされたタイミングをフックすることができます。
また、ワイルドカードを使って、各プラグインがロードされるたびに共通の処理を一括でフックすることも可能です。</p>
<pre><code data-lang="typescript">// 各プラグインがロードされるたびに、そのプラグイン名をログに出す例
await autocmd.define(
  denops,
  &quot;User&quot;,
  &quot;DvpmPluginLoadPost:*&quot;,
  `echom &quot;Loaded plugin: &quot; . substitute(expand(&quot;&lt;amatch&gt;&quot;), &quot;^DvpmPluginLoadPost:&quot;, &quot;&quot;, &quot;&quot;)`,
);
</code></pre>
<p>他にも <code>DvpmBeginPre/Post</code>, <code>DvpmEndPre/Post</code>, <code>DvpmCacheUpdated</code> といったシステム全体のライフサイクルに合わせたイベントも用意されています。</p>
<div class="message"><p><strong>注意: フック登録のタイミング</strong></p>
<p><code>dvpm</code> は <code>denops.vim</code> 上で動作するため、各種フック（コマンド、イベント、ファイルタイプ、キー）が <code>denops</code> 側で登録完了するまでは、たとえ条件を満たしていてもプラグインはロードされません。起動直後の極めて早いタイミングで発生するイベントなどには注意が必要です。</p>
</div>
<h2 id="まとめ">まとめ</h2>
<p>キャッシュ機能と遅延ロード機能が加わったことで、 <code>dvpm</code> は「TypeScript で Vim / Neovim の設定が書ける」という変態的なメリットを維持したまま、現代的なプラグインマネージャーとしての利便性と、圧倒的な起動速度の両立を手に入れました。</p>
<p>「やっぱり設定は型安全に TypeScript で書きたい！」という方は、ぜひ試してみてください。</p>
<p><a rel="external" href="https://github.com/yukimemi/dvpm">yukimemi/dvpm: dvpm - Denops Vim/Neovim Plugin Manager</a></p>
]]></content:encoded>
      </item>
    </channel>
</rss>
