zudo-paper

Terminal / PTY / IPC / xterm.js の概念整理メモ

Author: Takazudo | 作成: 2026/03/07

概要

現在、ターミナル機能を内蔵したElectronアプリを開発している。xterm.js + node-ptyでターミナルを埋め込む実装を進める中で「PTYとは何か」「IPCとは何か」「xterm.jsは何をしているのか」みたいな基本概念を調べたので、開発メモとしてまとめたもの。

出発点: package.jsonの依存関係

Electronアプリのpackage.jsonに以下のようなパッケージが入っているのを見て、「これらは何をしているのか」というところから始まった。

  • xterm — ブラウザ上でターミナルを描画するライブラリ
  • node-pty — Node.jsからOSのPTYを操作するバインディング
  • @xterm/addon-fit — xterm.jsのターミナルサイズをコンテナに合わせるアドオン

それぞれの役割を理解するには、まずPTYとIPCという概念を把握する必要がある。

PTY(Pseudo-Terminal)とは

PTYは「擬似端末」と訳される、OSカーネルが提供する仕組み。物理的なターミナルデバイスをソフトウェアでエミュレートするもの。

PTYにはmaster側とslave側がある。

  • master側 — ターミナルを制御するプログラム側(node-ptyがここに位置する)
  • slave側 — 本物のターミナルに見えるシェル側(zshなど)

シェルは自分がPTYの中にいることを知らない。ANSIエスケープコード、Ctrl+Cによるシグナル送信、パスワード入力の非表示など、物理端末で動作するものはすべてPTY経由でも動作する。

ここでNode.jsのchild_process.spawnとの違いが出てくる。spawnはraw pipeでI/Oをやり取りするだけなので、上記のようなターミナル機能は使えない。PTYを経由することで、シェルが「自分は本物のターミナルに接続されている」と認識する。

IPC(Inter-Process Communication)とは

IPCはプロセス間通信のこと。Electron固有の概念ではなく、OSレベルの一般的な仕組み。pipe、socket、shared memory、signalなどもIPCの一種。

Electronにおいては、main process(Node.js)とrenderer process(Chromium)がセキュリティのため分離されている。この2つのプロセスが通信するためにipcMain / ipcRendererというAPIが用意されている。ターミナルの実装では、このIPCがキー入力とシェル出力の橋渡しをする。

Electronアプリにおける3つのプロセス

Electronでターミナルを実装すると、3つのプロセスが関わる。

Electron App
├── Main Process (Node.js)
│   └── node-pty → spawns zsh (3rd process)

├── Renderer Process (Chromium/browser)
│   └── xterm.js (draws terminal on screen)

└── IPC bridge connects them

データの流れはこうなる。

キー入力 → xterm.js (renderer) → IPC → main process → node-pty → zsh
zsh出力 → node-pty → main process → IPC → xterm.js (renderer) → 画面描画

シェル(zsh)はElectronとは別の第3のプロセスとして動いている。node-ptyがPTYのmaster側を持ち、zshがslave側に接続されるという構成。

Terminal / TTY / PTYの語源

これらの用語は歴史的な経緯で名前が付いている。

  • Terminal — 元々は物理的なキーボード+スクリーンのデバイス。1970年代のVT100などが有名
  • TTY — "teletypewriter"の略。Terminalよりさらに古い、テレタイプ端末が由来。Unixでは/dev/ttyが現在のターミナルデバイスを指す
  • PTY — TTYの一種で、ソフトウェアで実装された疑似端末。物理デバイスを持たず、プログラム同士の接続に使われる
  • xterm — 1984年のX Window System向けターミナルエミュレータが元ネタ。npmのxtermパッケージ(xterm.js)はその名前を借用している

ネイティブターミナルアプリとの違い

iTerm2Ghostty、Terminal.appのようなネイティブターミナルアプリと、Electron + xterm.jsの違いを整理する。

  • iTerm2 — Objective-C/Swift製。Core Text + Metalでレンダリング。ブラウザエンジンは使っていない
  • Ghostty — Zigで書かれた独自エンジン。Metal/OpenGLでレンダリング。VTパーサーもレンダラーもフルスクラッチ
  • Terminal.app — Apple製。Objective-C/Swift + macOSネイティブフレームワーク

レンダリング層はアプリごとに全く違うが、PTY層は全員同じOSの仕組みを使っている。

[App-specific renderer]  ← アプリごとに異なる

    [OS PTY API]          ← 全アプリ共通のOS機構

    [zsh/bash]            ← シェルプロセス

つまりターミナルアプリの違いは「どうやって画面に描画するか」だけで、シェルとの通信方法は同じということになる。

xterm.jsの描画方式

xterm.jsはHTML + CSSでテキストを描画しているわけではない。Canvas要素にテキストを描画している。WebGLアドオンを使うとさらに高速化できる。

DOM要素で数千文字のターミナル出力をフォーマットすると遅すぎるため、Canvasに直接描画する方式が採用されている。CSSでスタイルできるのはコンテナ(サイズ、ボーダー等)のみで、ターミナル内部の色、フォント、カーソルなどはJS APIで設定する。

const term = new Terminal({
  fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
  fontSize: 14,
  theme: {
    background: "#1e1e2e",
    foreground: "#cdd6f4",
    cursor: "#f5e0dc",
  },
});

「CSSで色を変えたい」と思ってもできない。themeオブジェクトで設定する必要がある。

フォント未インストールによる表示崩れ

xterm.jsでよく遭遇する問題として、指定フォントがインストールされていない場合の表示崩れがある。

上記の設定例だとJetBrains Mono、Fira Code、Cascadia Codeの順にフォールバックして、最終的にシステムのmonospaceに到達する。ここで、絵文字やPowerline記号がダブル幅で描画されるのに対し、xterm.jsがシングル幅と想定するためズレが発生する。

Electron アプリのターミナル表示崩れ

「Cl aude Code」のように文字間に隙間ができたり、ステータスバーの記号が位置ズレしたりする。フォントの幅計算とxterm.jsの文字幅想定が一致しないことが原因。指定したフォントが確実にインストールされている環境でないと、この手の問題が起きる。

まとめ

Electronアプリにターミナルを埋め込む構成を整理すると、以下の3層になる。

  • renderer process(xterm.js)— 画面描画とキー入力の受け取り
  • main process(node-pty)— PTYのmaster側としてシェルを生成・管理
  • シェルプロセス(zsh等)— PTYのslave側として動作

PTY、IPC、プロセス分離といった低レイヤーの概念を理解しておくと、問題が起きたときに「これはレンダリングの問題なのか、PTYの問題なのか、IPCの問題なのか」を切り分けやすくなる。