さとまたwiki

⚡ Electron入門

VS CodeもSlackもDiscordも、全部Electronで作られている

Electron v33
デスクトップアプリ基盤
🖥️
node-pty
疑似端末(PTY)制御
📺
xterm.js
ターミナルUI描画

Section 1: フォルダ構成を最初に見よう【最重要】

Electronアプリを理解する最速の方法は、フォルダ構成から全体像をつかむこと。コードを読む前にまずここを見よう。

book_app/                    ← アプリのルートフォルダ
│
├── main.js                  ← 🧠 メインプロセス(Node.js側)
│                               ファイルの読み書き・シェル起動・
│                               ウィンドウ作成を担当(963行)
│
├── package.json             ← 📦 アプリの「設計書」
│                               使うライブラリと起動コマンドを定義
│
├── package-lock.json        ← 🔒 ライブラリのバージョン固定
│                               (自動生成・触らなくてOK)
│
├── icon.ico                 ← 🎨 Windowsタスクバーアイコン
│
├── src/
│   └── index.html           ← 🖥️ レンダラープロセス(ブラウザ側)
│                               画面デザイン+ターミナルUIを担当(3163行)
│
└── node_modules/            ← 📚 インストール済みライブラリ
    ├── electron/            ← Electron本体
    ├── node-pty/            ← ターミナル制御
    ├── @xterm/xterm/        ← ターミナルUI描画
    ├── @xterm/addon-fit/    ← ターミナル自動リサイズ
    └── @xterm/addon-web-links/ ← URL自動リンク化
main.js 🧠
  • • Node.js環境で動く
  • • ファイル読み書き(fs)
  • • シェル(PowerShell/Bash)起動
  • • ウィンドウ作成(BrowserWindow)
  • • IPCハンドラー40種以上
src/index.html 🖥️
  • • Chromiumブラウザで表示
  • • HTML/CSSでレイアウト
  • • xterm.jsでターミナルUI
  • • JavaScriptでUI操作
package.json 📦
  • • アプリ名・バージョン定義
  • • "main": "main.js" で起動ファイル指定
  • • 依存ライブラリ一覧
  • • 起動コマンド(scripts)
💡
ワンポイント

node_modules/ フォルダは絶対にGitにコミットしないこと! .gitignorenode_modules/ と書いておく。他のメンバーは npm install で再インストールできる。

Section 2: Electronとは3行で

VS Code Slack Discord Figma Twitch ← 全部Electronで作られている
  1. 1 ブラウザ(Chromium)= 画面を作る
  2. 2 サーバー(Node.js)= ファイルやOSを操作する
  3. 3 この2つをデスクトップアプリとして合体させたのがElectron

比較表

比較項目ブラウザWebアプリNode.jsサーバーElectronアプリ
動作環境ブラウザの中サーバーユーザーのPC
ファイル操作
画面(HTML/CSS)
OS操作
オフライン動作
配布方法URLでアクセスサーバー起動.exeインストール
💡
ワンポイント

ElectronはChromiumとNode.jsを丸ごと内包しているため、アプリサイズが大きくなりやすい(最低でも100MB程度)。でもその分、一度インストールすれば外部依存なしで動く。

Section 3: package.jsonを読む

プロジェクトの「設計書」となるファイル。コメント付きで読んでみよう。

{
  "name": "book-dashboard",       // アプリ名(英小文字・ハイフンのみ)
  "version": "1.0.0",             // バージョン番号
  "description": "書籍管理ダッシュボード",
  "main": "main.js",              // ← ここが起動ファイル!必須
  "scripts": {
    "dev": "electron .",          // npm run dev で起動
    "start": "electron ."         // npm start でも起動
  },
  "dependencies": {               // 実行時に必要なライブラリ
    "@xterm/addon-fit": "^0.10.0",
    "@xterm/addon-web-links": "^0.11.0",
    "@xterm/xterm": "^5.5.0",
    "node-pty": "^1.0.0"
  },
  "devDependencies": {            // 開発時だけ必要なライブラリ
    "electron": "^33.2.1"
  }
}

パッケージ解説

パッケージ役割なぜ必要?
electronデスクトップアプリ基盤Chromium+Node.js本体
node-pty疑似端末(PTY)作成本物のシェルを起動するため
@xterm/xtermターミナルUI描画ブラウザ上でターミナルを表示
@xterm/addon-fit自動リサイズウィンドウサイズに追従
@xterm/addon-web-linksURLリンク化ターミナル内URLをクリッカブルに
⚠️
注意

"main": "main.js" がないと、Electronは何を起動すればいいかわからずエラーになる。絶対に忘れずに。

npm install   # 依存関係をインストール(初回のみ)
npm run dev   # アプリを起動

Section 4: 2つのプロセスを理解する

Electronの最重要概念。メインプロセスとレンダラープロセスという2つの世界が存在する。

┌──────────────────────────────────────────────────────┐
│                  Electronアプリ                        │
│                                                       │
│  ┌───────────────────┐    ┌───────────────────────┐  │
│  │  メインプロセス    │    │  レンダラープロセス    │  │
│  │  (main.js)        │◄──►│  (src/index.html)     │  │
│  │                   │IPC │                       │  │
│  │  • Node.js で動く │    │  • Chromiumで動く     │  │
│  │  • ファイル読み書き│    │  • HTML/CSS/JS        │  │
│  │  • シェル起動      │    │  • ターミナルUI       │  │
│  │  • ウィンドウ管理  │    │  • ユーザー操作       │  │
│  └───────────────────┘    └───────────────────────┘  │
│                                                       │
│    1つのアプリに「Node.js」と「ブラウザ」が共存!     │
└──────────────────────────────────────────────────────┘

実際のmain.js冒頭(コメント付き)

// Electronから必要な機能を取り込む
const { app, BrowserWindow, ipcMain, shell } = require('electron');
const path = require('path');
const fs   = require('fs');        // ファイル読み書き
const pty  = require('node-pty'); // シェル起動
const os   = require('os');        // OS情報取得

let mainWindow;  // ウィンドウを変数に保持(後でIPC通信に使う)
let ptyProcess;  // ターミナル(PTY)を変数に保持

// ウィンドウを作る関数
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1400,            // 横幅(ピクセル)
    height: 900,            // 縦幅(ピクセル)
    title: 'Book Dashboard',
    backgroundColor: '#1a1a2e', // ダーク背景(ちらつき防止)
    webPreferences: {
      nodeIntegration: true,   // レンダラーでNode.jsを使う設定
      contextIsolation: false, // セキュリティ分離を無効にする設定
    }
  });
  mainWindow.loadFile('src/index.html'); // HTMLを読み込んで表示
}

// Electronが起動完了したらウィンドウを作る
app.whenReady().then(() => {
  createWindow();
  createPty();  // ターミナルも同時に起動
});

// 全ウィンドウが閉じられたらアプリ終了(Mac以外)
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});
⚠️
セキュリティ注意

nodeIntegration: true はレンダラー(HTML画面)側からファイルシステム等にアクセスできる便利な設定だが、セキュリティリスクがある。外部URLを表示しないローカルアプリ限定の設定。本格アプリでは preload.js + contextBridge 方式を使うこと。

💡
ワンポイント

app.whenReady() は Electron が OSへの登録を完了したタイミングで発火する。このタイミング前にウィンドウを作ろうとするとエラーになる。

Section 5: ターミナルUIの仕組み

Book Dashboardの核心機能。node-ptyとxterm.jsを組み合わせて「本物のシェルが動くターミナルUI」を実現している。

┌────────────────────────────────────────────────────────┐
│                  ターミナル実装の仕組み                   │
│                                                         │
│  【node-pty】           IPC通信        【xterm.js】      │
│  メインプロセス側                      レンダラー側      │
│                                                         │
│  ┌──────────────────┐  terminal-data  ┌─────────────┐  │
│  │  PowerShellを    │ ──────────────► │  文字を     │  │
│  │  起動・制御する   │                 │  画面に表示  │  │
│  │                  │ ◄────────────── │             │  │
│  │  コマンドを実行  │  terminal-input │  キー入力を  │  │
│  │  して結果を返す  │                 │  受け取る    │  │
│  └──────────────────┘                 └─────────────┘  │
│         ↑                                               │
│    本物のシェルが動く                                     │
└────────────────────────────────────────────────────────┘

Step 1: node-ptyでシェルを起動(main.js)

const pty = require('node-pty');
const os  = require('os');

function createPty() {
  // OSに合わせてシェルを選択
  const shell = os.platform() === 'win32'
    ? 'powershell.exe'  // Windows
    : 'bash';           // Mac/Linux

  // 疑似端末(PTY)を作成してシェルを起動
  ptyProcess = pty.spawn(shell, [], {
    name: 'xterm-color',  // カラー対応ターミナル
    cols: 80,             // 横幅(文字数)
    rows: 30,             // 縦幅(行数)
    cwd: 'C:\\Users\\...\\book', // 起動ディレクトリ
    env: process.env,     // 環境変数をそのまま引き継ぐ
    useConpty: false,     // Windows: 古い互換モード(安定性向上)
  });

  // シェルの出力をレンダラーに転送
  ptyProcess.onData((data) => {
    if (mainWindow && !mainWindow.isDestroyed()) {
      mainWindow.webContents.send('terminal-data', data);
    }
  });
}
つまずきポイント:node-ptyのビルドエラー

node-ptyはネイティブモジュール(C++のコンパイルが必要なライブラリ)。npm install 後にエラーが出たら Electron 用に再コンパイルが必要。

npm install --save-dev @electron/rebuild
./node_modules/.bin/electron-rebuild

Step 2: xterm.jsでターミナルUIを表示(src/index.html)

const { ipcRenderer, shell, clipboard } = require('electron');
const { Terminal }    = require('@xterm/xterm');
const { FitAddon }    = require('@xterm/addon-fit');
const { WebLinksAddon } = require('@xterm/addon-web-links');

// ターミナルインスタンスを作成
const terminal = new Terminal({
  theme: {
    background: '#1e1e1e', // VS Codeライクなダーク背景
    foreground: '#d4d4d4', // 文字色(明るいグレー)
    cursor:     '#d4d4d4', // カーソル色
  },
  fontSize:   14,
  fontFamily: 'Consolas, monospace', // 等幅フォント必須
  cursorBlink: true,                 // カーソルを点滅させる
  scrollback:  50000,                // スクロールで遡れる行数
});

const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);  // リサイズ対応

// URLをクリックでブラウザを開く
terminal.loadAddon(new WebLinksAddon((e, uri) => {
  shell.openExternal(uri);
}));

// HTMLのdivにマウント
terminal.open(document.getElementById('terminal'));
fitAddon.fit(); // 親要素のサイズに自動フィット

Step 3: IPC通信でつなぐ

// キー入力 → メインプロセス → シェルへ
terminal.onData((data) => {
  ipcRenderer.send('terminal-input', data);
});

// シェルの出力 → ターミナルUIへ
ipcRenderer.on('terminal-data', (event, data) => {
  terminal.write(data);
});

Step 4: リサイズ対応(デバウンス付き)

// 連続リサイズ中に何度も処理しないよう50ms待ってから実行
let fitTimeout = null;
function debouncedFit() {
  if (fitTimeout) clearTimeout(fitTimeout);
  fitTimeout = setTimeout(() => {
    fitAddon.fit(); // UIを親要素に合わせる
    // シェル側にも新しいサイズを通知(これを忘れると折り返しがずれる)
    ipcRenderer.send('terminal-resize', {
      cols: terminal.cols,
      rows: terminal.rows
    });
  }, 50);
}
window.addEventListener('resize', debouncedFit);
new ResizeObserver(debouncedFit).observe(document.getElementById('terminal'));
💡
ワンポイント

fitAddon.fit() はxterm.jsの表示サイズをDOMに合わせるだけ。シェル側にも terminal-resize でサイズを伝えないと、コマンドの折り返し位置がずれる。2つセットで実行すること。

Section 6: IPC通信の2つのパターン

メインプロセスとレンダラープロセスは直接関数を呼び合えない。IPC(Inter-Process Communication)というメッセージのやり取りで通信する。

パターン1: send / on(返値なし・速い)

// ===== レンダラー側(index.html) =====
ipcRenderer.send('terminal-input', 'ls -la'); // 送るだけ・返値なし

// ===== メイン側(main.js) =====
ipcMain.on('terminal-input', (event, data) => {
  ptyProcess.write(data); // シェルにコマンドを書き込む
});

パターン2: invoke / handle(返値あり・Promise)

// ===== レンダラー側(index.html) =====
const projects = await ipcRenderer.invoke('load-projects'); // awaitで待つ

// ===== メイン側(main.js) =====
ipcMain.handle('load-projects', () => {
  const data = fs.readFileSync('projects.json', 'utf-8');
  return JSON.parse(data); // returnした値がレンダラーのawaitに届く
});

使い分け

パターン使う場面特徴
send / onターミナル入力、リサイズ通知返値なし・速い
invoke / handleファイル読み込み・データ保存返値あり・Promise

このアプリの主要IPCハンドラー一覧

チャンネル名種類説明
terminal-inputsend/onキー入力をシェルへ転送
terminal-resizesend/onターミナルサイズ変更をシェルへ通知
terminal-dataメイン→レンダラーシェル出力をUIへ転送
load-projectsinvoke書籍プロジェクト一覧を読み込む
create-new-bookinvoke新規書籍フォルダ+ファイルを作成
run-ai-generateinvokeClaudeへのAI生成指示を送信
count-wordsinvokeMarkdown文字数カウント
open-folderinvokeエクスプローラーでフォルダを開く
convert-for-notebooklminvokeMD→NotebookLM形式に変換

合計40種類以上

💡
ワンポイント

チャンネル名('terminal-input'等)はただの文字列。スペルミスしてもエラーが出ないので注意。送信側と受信側のチャンネル名が一致していることを確認しよう。

Section 7: Bracket Paste Mode — 長文を安全に送る

AIに長いプロンプトを送るとき、普通にテキストを流し込むと途中で誤実行されることがある。これを防ぐ技法。

// ❌ 普通の方法:長いプロンプトが途中で誤認識される場合がある
ptyProcess.write(longPrompt + '\r');

// ✅ Bracket Paste Modeで包む(推奨)
// \x1b[200~ = 「ここから貼り付け開始」のエスケープシーケンス
// \x1b[201~ = 「貼り付け終了」のエスケープシーケンス
// \r        = Enterキー
ptyProcess.write('\x1b[200~' + longPrompt + '\x1b[201~\r');
💡
ワンポイント

\x1b はエスケープ文字(ESC)。\x1b[200~ のような文字列を「エスケープシーケンス」と呼ぶ。Bracket Paste Modeは現代的なシェルがサポートしている標準機能で、多行テキストを安全に貼り付けられる。

Section 8: UIレイアウト設計

デスクトップアプリらしい「スクロールなし・全画面」UIの作り方。

┌──────────────────────────────────────────────────────┐
│  HEADER(固定)                                       │
│  統計 | [Claude起動] [Sonnet起動] [Opus起動] [新規作成]│
├──────────────┬───────────────────────────────────────┤
│              │  [ターミナル] [詳細] [コメント](タブ)  │
│  SIDEBAR     ├───────────────────────────────────────┤
│  (300px固定)│  ターミナルタブ:                      │
│              │  ┌─────────────────────────────────┐ │
│  ・フィルタ  │  │  xterm.js ターミナル             │ │
│  ・書籍リスト│  │                                  │ │
│  (縦スクロール)│  │  PS C:\book> claude             │ │
│              │  │  ▌                               │ │
│              │  └─────────────────────────────────┘ │
└──────────────┴───────────────────────────────────────┘

主要CSS

body {
  display: flex;
  flex-direction: column;
  height: 100vh;     /* 画面全体の高さを使う */
  overflow: hidden;  /* スクロールバーを消す */
}
.main { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 300px; overflow-y: auto; } /* 縦スクロール可能 */
.content { flex: 1; display: flex; flex-direction: column; }
#terminal-panel { flex: 1; overflow: hidden; } /* xtermが内部でスクロール管理 */

Section 9: Ctrl+C/V のカスタマイズ

ターミナルのCtrl+CはSIGINT(プロセス中断)として機能するが、テキストをコピーしたい場合もある。条件分岐でうまく切り分ける。

terminal.attachCustomKeyEventHandler((e) => {
  if (e.ctrlKey && e.key === 'c') {
    const selected = terminal.getSelection();
    if (selected) {
      clipboard.writeText(selected); // 選択テキストをコピー
      terminal.clearSelection();
      return false; // xterm.jsのデフォルト処理をキャンセル
    }
    return true; // 選択なし → SIGINT(プロセス中断)としてシェルに通す
  }
  if (e.ctrlKey && e.key === 'v') {
    const text = clipboard.readText();
    if (text) ipcRenderer.send('terminal-input', text);
    return false;
  }
  return true;
});
💡
ワンポイント

return false で xterm.js のデフォルト処理をキャンセル、return true で通す。この切り分けで「コピーしたいとき」と「SIGINTを送りたいとき」を分けられる。

Section 10: まとめ

Electronアプリ開発の流れと重要概念を整理する。

開発の流れ

  1. 1 package.json を作り "main": "main.js" を指定する
  2. 2 main.jsBrowserWindow を作り HTMLを読み込む
  3. 3 必要なら node-pty でシェルを起動する
  4. 4 src/index.htmlxterm.js を使いターミナルUIを作る
  5. 5 ipcMain / ipcRenderer で2つのプロセスをつなぐ
  6. 6 npm run dev で起動

重要概念振り返り

概念一言説明
メインプロセスNode.jsが動くOS制御側(main.js)
レンダラープロセスChromiumが動く画面表示側(index.html)
IPC通信2プロセス間のメッセージやり取り
node-pty本物のシェルを起動・制御するライブラリ
xterm.jsターミナルの画面を描画するライブラリ
Bracket Paste Mode長文を安全にシェルに送る技法

参考リンク