設定・プロファイル・CLI:Hyperのカスタマイズの仕組み
前提知識
- ›第1回:アーキテクチャとプロジェクト構造
- ›第5回:プラグインシステム詳解
- ›XDG Base Directory仕様の基本的な理解
設定・プロファイル・CLI:Hyperのカスタマイズの仕組み
このシリーズを通じて、設定データがほぼすべてのサブシステムを流れていく様子を見てきました。メインプロセスが読み込み、プラグインシステムがデコレートし、レンダラーがxterm.jsに適用し、RPCブリッジがプロセス間の更新を仲介します。これまでそれを「ブラックボックス」として扱ってきました。シリーズ最終回となる今回は、いよいよそのボックスの中を開きます。hyper.jsonの構造と多段階マージパイプラインによる設定の組み立て、プロファイルシステムによるコンテキスト別のオーバーライドを見ていきます。さらに、独立したCLIツールがconfig fileを直接編集してプラグインを管理する仕組みも解説します。
Config Fileの形式とファイルシステムのレイアウト
Hyperの設定は、4つのトップレベルキーを持つ単一のJSONファイルに集約されています。
app/config/config-default.json
{
"$schema": "./schema.json",
"config": { /* terminal settings */ },
"plugins": [],
"localPlugins": [],
"keymaps": {}
}
configキーにはフォント、カラー、シェルパス、スクロールバック、カーソルスタイルといったターミナル設定が収まります。plugins配列にはインストールするnpmパッケージ名を列挙し、localPluginsはローカルプラグインフォルダ内のディレクトリを参照します。keymapsはキーボードショートカットのオーバーライドに使います。
ファイルシステム上のレイアウトはapp/config/paths.tsで定義されています。
| パス | プラットフォーム | 場所 |
|---|---|---|
| Config dir | macOS/Linux | ~/.config/Hyper/(または$XDG_CONFIG_HOME/Hyper/) |
| Config dir | Windows | %APPDATA%/Hyper/ |
| Config file | 全プラットフォーム | <config dir>/hyper.json |
| Plugins dir | 全プラットフォーム | <config dir>/plugins/ |
| Local plugins | 全プラットフォーム | <config dir>/plugins/local/ |
| Plugin cache | 全プラットフォーム | <config dir>/plugins/cache/ |
| Dev override | 開発モード | <repo root>/hyper.json |
XDGサポートも注目ポイントです。$XDG_CONFIG_HOMEが設定されていればHyperはそれを優先します。開発モードでは、リポジトリルートにhyper.jsonが存在する場合、config pathがそちらに上書きされます。これにより、グローバル設定を変更せずに設定の動作確認ができます。
ヒント:
app/config/schema.jsonは、typescript-json-schema ./typings/config.d.ts rawConfigコマンドによってTypeScript型定義から自動生成されています。デフォルトconfigにある"$schema"の参照は、VS Codeなどのスキーマ対応エディタでのオートコンプリートを有効にします。
Configの読み込みとマージパイプライン
config読み込みは_import()から始まり、複数のステージを経て処理されます。
flowchart TD
A["Read config-default.json"] --> B["Read platform keymaps\n(darwin.json / win32.json / linux.json)"]
B --> C["Read user hyper.json"]
C --> D["Attempt legacy migration\n(.hyper.js → hyper.json)"]
D --> E["_init(): Deep merge\ndefaults + user config"]
E --> F["Normalize profiles\n(ensure default profile exists)"]
F --> G["Merge platform keymaps\nwith user keymaps"]
G --> H["Filter null/undefined\nfrom plugin arrays"]
H --> I["parsedConfig object"]
マージ処理は_init関数が担当します。
ディープマージにはLodashのmerge()を使用しており、オブジェクトは再帰的にマージされる一方、配列は完全に置き換えられます。プロファイルの正規化では、設定に必ず「default」という名前のプロファイルが空のconfigオブジェクトとともに存在するよう保証します。
keymapsのマージはよりシンプルで、スプレッド構文によりユーザー設定がプラットフォームのデフォルトを上書きします:{...defaultCfg.keymaps, ...userCfg?.keymaps}。その後、mapKeysがキーの組み合わせを正規化します(配列への変換や非推奨フォーマットの処理などを含みます)。
エラー処理も堅牢です。hyper.jsonのパースに失敗した場合はデフォルト設定が使われ、通知が表示されます。デフォルトファイル自体が見つからない場合は、空オブジェクトをフォールバックとして使用します。
プロファイルシステム
プロファイルを使うと、用途ごとに異なるシェル、カラー、フォントといった設定をオーバーライドできます。プロファイルの解決はapp/config.ts#L97-L108で行われます。
export const getProfileConfig = (profileName: string): configOptions => {
const {profiles, defaultProfile, ...baseConfig} = cfg.config;
const profileConfig = profiles.find((p) => p.name === profileName)?.config || {};
for (const key in profileConfig) {
if (typeof baseConfig[key] === 'object' && !Array.isArray(baseConfig[key])) {
baseConfig[key] = {...baseConfig[key], ...profileConfig[key]};
} else {
baseConfig[key] = profileConfig[key];
}
}
return {...baseConfig, defaultProfile, profiles};
};
flowchart LR
A["Base Config\n(all keys except profiles)"] --> D["Merge Strategy"]
B["Profile Config\n(profile-specific overrides)"] --> D
D --> E{"Value type?"}
E -->|"Object (not Array)"| F["Shallow merge\n{...base, ...profile}"]
E -->|"Scalar or Array"| G["Replace entirely"]
F --> H["Final Config"]
G --> H
マージ戦略はオブジェクトに対して意図的に シャロー です。たとえばプロファイルのcolorsオーバーライドはベースのcolorsとシャローマージされるため、変更したい色だけを指定すれば済みます。ただしshellArgsのような配列は結合ではなく完全置換になるため、注意が必要です。
プロファイル固有のコマンドはapp/commands.ts#L150-L163で自動生成されます。
getConfig().profiles.forEach((profile) => {
commands[`window:new:${profile.name}`] = () => {
setTimeout(() => app.createWindow(undefined, undefined, profile.name), 0);
};
commands[`tab:new:${profile.name}`] = (focusedWindow) => {
focusedWindow?.rpc.emit('termgroup add req', {profile: profile.name});
};
// ... pane:splitRight, pane:splitDown
});
各プロファイルは自動的に、そのプロファイルの設定で新しいウィンドウ・タブ・スプリットペインを開くコマンドを持ちます。これらはkeymaps設定でキーボードショートカットに割り当てることができます。
Configの監視と変更の伝播
設定の変更はchokidarによるファイル監視で検知されます。
ウォッチャーはsetTimeoutによる100msのデバウンスを設けており、ファイルへの書き込みが完了してから読み込みを行います。変更が検知されると、その影響はシステム全体に連鎖して伝播します。
sequenceDiagram
participant FS as hyper.json
participant CW as Chokidar Watcher
participant CF as Config Module
participant P as Plugin System
participant W as BrowserWindow
participant R as Renderer
FS->>CW: File changed
CW->>CF: onChange callback (100ms debounce)
CF->>CF: Re-import and parse config
CF->>CF: Notify all subscribers
Note over CF: Subscriber 1: Menu rebuild
CF->>W: Menu.setApplicationMenu(newMenu)
Note over CF: Subscriber 2: Plugin check
CF->>P: Compare JSON.stringify(plugins)
P->>P: If changed → updatePlugins()
Note over CF: Subscriber 3: Window update
CF->>W: webContents.send('config change')
W->>R: Renderer receives config change
R->>R: config.subscribe callback
R->>R: store.dispatch(reloadConfig(newConfig))
R->>R: React re-renders with new config
サブスクライバーのパターンはシンプルで、コールバックの配列とsubscribe関数で構成されています。subscribeは登録解除用の関数を返します。
export const subscribe = (fn: Function) => {
watchers.push(fn);
return () => {
watchers.splice(watchers.indexOf(fn), 1);
};
};
各BrowserWindowはapp/ui/window.ts#L78-L93でconfig変更を購読します。レンダラーに'config change'イベントを送るとともに、シェルの設定が変わったかどうかもチェックします。変更されていた場合は、新しいシェルを使うには新しいタブを開く必要があることをユーザーに通知します。
レガシーマイグレーションとスキーマ生成
Hyper 3ではmodule.exportsを使ったJavaScriptのconfig file(.hyper.js)が使われていました。Hyper 4でJSON(hyper.json)に移行し、app/config/migrate.tsがこの移行を処理します。
app/config/migrate.ts#L147-L190
マイグレーションはhyper.jsonが存在せず.hyper.jsがある場合にのみ実行されます。処理の流れは次のとおりです。
.hyper_plugins/localからローカルプラグインを新しい場所にコピーvm.ScriptでJS configをパースし、エクスポートされたオブジェクトを取り出す- デフォルトとディープマージして完全なJSON configを生成
- JSON化できない設定(計算された値や関数)を、
recastによるAST変換でmigrated-hyper3-config.jsというローカルプラグインに抽出 - 新しい
hyper.jsonを書き出す
特に巧妙なのがconfigToPlugin関数です。AST操作によって旧JS configからJSON非シリアライズな式を取り出し、decorateConfigプラグインとしてラップします。これにより、移行後もその設定が透過的に機能し続けます。
flowchart TD
A[".hyper.js exists\nhyper.json missing"] --> B["Parse JS with vm.Script"]
B --> C["Extract module.exports"]
C --> D["Deep merge with defaults"]
D --> E["Write hyper.json"]
C --> F["AST-analyze for non-JSON values"]
F --> G{"Has computed/function values?"}
G -->|Yes| H["Generate migrated-hyper3-config.js\nas local plugin"]
G -->|No| I["Done"]
H --> J["Add to localPlugins array"]
J --> I
CLIツール:プラグイン管理
cli/index.tsはElectronアプリに同梱されるスタンドアロンのNode.jsツールです。hyper.jsonを直接読み書きしてプラグインを管理するため、実行中のアプリとのIPC通信は行いません。
CLIは6つのサブコマンドを提供します。
| コマンド | エイリアス | 動作 |
|---|---|---|
hyper install <plugin> |
i |
npmで存在を確認してplugins配列に追加 |
hyper uninstall <plugin> |
u, rm, remove |
plugins配列から削除 |
hyper list |
ls |
インストール済みプラグインを表示 |
hyper search <query> |
s |
npms.ioでhyper-plugin/hyper-themeパッケージを検索 |
hyper list-remote |
lsr, ls-remote |
利用可能な全プラグインを一覧表示 |
hyper docs <plugin> |
d, h, home |
プラグインのnpmページを開く |
cli/api.ts#L100-L119のinstallフローでは、configに追加する前にnpm上でパッケージの存在を確認します。
function install(plugin, locally?) {
return existsOnNpm(plugin)
.catch((err) => {
if (statusCode === 404 || statusCode === 200) {
return Promise.reject(`${plugin} not found on npm`);
}
return Promise.reject(`Plugin check failed...`);
})
.then(() => {
if (isInstalled(plugin, locally)) {
return Promise.reject(`${plugin} is already installed`);
}
const config = getParsedFile();
config[locally ? 'localPlugins' : 'plugins'] = [...array, plugin];
save(config);
});
}
サブコマンドが指定されない場合、CLIはHyper自体を起動します。macOSではopen -b co.zeit.hyperを使って複数インスタンスの起動を防ぎます。それ以外のプラットフォームでは、Electronをデタッチされた子プロセスとして起動します。
起動時に環境変数HYPER_CLI=1が設定され、CLIから起動されたことをElectronアプリに伝えます。またELECTRON_NO_ATTACH_CONSOLE=1により、ElectronプロセスがCLIのコンソールを引き継がないようにして、ターミナルの表示をクリーンに保ちます。
ヒント: CLIはメモ化(memoization)を使ってconfig fileを遅延読み込みします(
cli/api.tsを参照)。そのためdocsやversionのようにconfigを必要としないサブコマンドは、config fileが存在しない場合や壊れている場合でも正常に動作します。
シリーズのまとめ
6回の記事を通じて、Hyperのアーキテクチャ全体を追いかけてきました。3プロセス設計とwebpackビルドツール、型付きRPCブリッジとターミナルセッションのライフサイクル、ReactをバイパスするRedux middlewareチェーンを見てきました。さらに、WebGLフォールバックを備えたxterm.jsコンポーネントのラッパー、38のextension pointを持つプラグインシステム、そしてそれらをすべてつなぎ合わせる設定パイプラインも解説しました。
Hyperはある意味で興味深いアーキテクチャの選択を体現しています。パフォーマンスが重要なアプリケーションにWebテクノロジーを採用しながら、その抽象化レイヤーによって生じたパフォーマンスの損失を取り戻すために巧妙な最適化(V8スナップショット、write middleware、DataBatcher、DOM保持)を積み上げているのです。Electronアプリの開発やプラグインシステムの設計に興味がある方にとって、このコードベースに登場するパターンはどれも応用する価値があります。サービスロケーターオブジェクト、UUIDスコープのチャンネル、error-boundaryのデコレーションチェーン、イミュータブルなツリー状態など、自分のプロジェクトに活かせるものばかりです。