Ink
React for CLIs — ターミナル UI をコンポーネントで構築するフレームワーク
Ink は React のコンポーネント体験をコマンドラインアプリにそのまま持ち込むフレームワーク。内部では Facebook 製の Yoga Flexbox エンジンを使ってターミナルのレイアウトを計算し、chalk / ANSI 経由でテキストスタイルを適用する。
React のすべての機能 — Suspense・Concurrent Rendering・useTransition ・Context・カスタムフック — がそのまま使える。
npm install ink react
# スターターテンプレート
npx create-ink-app my-app # JavaScript
npx create-ink-app --typescript my-app # TypeScript
// counter.jsx
import React, {useState, useEffect} from 'react';
import {render, Text} from 'ink';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => setCount(c => c + 1), 100);
return () => clearInterval(timer);
}, []);
return <Text color="green">{count} iterations</Text>;
};
render(<Counter />);
Ink は カスタム React レンダラー として実装されている。ブラウザの DOM の代わりに独自の仮想 DOM を持ち、Yoga でレイアウトを計算してから ANSI 文字列を生成する。
log-update が前フレームと差分計算し、変更行だけ書き換えることでちらつきを抑制する。
| ソースファイル | 責務 |
|---|---|
reconciler.ts | react-reconciler を使ったカスタムレンダラー。DOM ノードの生成・更新・削除を管理。 |
dom.ts | Ink 独自の仮想 DOM 型定義(DOMNode / DOMElement)。 |
styles.ts | JSX props を Yoga スタイルにマッピング。Flexbox・padding・margin・border・color 等。 |
render-node-to-output.ts | DOM ツリーを ANSI 文字列に変換。境界・背景・テキスト装飾を適用。 |
log-update.ts | 前フレームとの差分を計算して stdout に書き出す。ちらつき抑制。 |
ink.tsx | アプリルート。イベントループ管理・stdin/stdout セットアップ・FPS 制御。 |
renderer.ts | Yoga レイアウト計算のトリガーと出力バッファ管理。 |
input-parser.ts | stdin バイト列をキーイベントにパース。kitty keyboard protocol 対応。 |
<Text> 内に書く必要がある。<Box> は Flexbox コンテナとして機能し、直接テキストを配置することはできない。
| コンポーネント | 役割 | 主要 Props |
|---|---|---|
<Text> |
テキスト表示・スタイリング |
color backgroundColor bold italic underline
strikethrough inverse dimColor wrap
|
<Box> |
Flexbox コンテナ<div style="display:flex"> 相当 |
flexDirection justifyContent alignItems flexWrap
gap padding margin width height
borderStyle borderColor backgroundColor overflow
|
<Newline> |
改行挿入<Text> 内でのみ使用可 |
count (default: 1) |
<Spacer> |
Flexbox の余白を埋める伸縮スペーサー | なし |
<Static> |
スクロールバッファに固定出力 完了ログ・通知など永続表示に使用 |
items (Array) style |
<Transform> |
子の文字列出力を変換 ライン単位で加工できる |
transform(outputLine, index) |
borderStyle に指定できる組み込み値:
single / double / round / bold /
singleDouble / doubleSingle / classic
またはカスタムオブジェクト(各辺の文字を個別指定)。
<Box borderStyle="round" borderColor="green" padding={1}>
<Text>Round green border</Text>
</Box>
<Box
borderStyle={{
topLeft: '╔', top: '═', topRight: '╗',
bottomLeft: '╚', bottom: '═', bottomRight: '╝',
left: '║', right: '║'
}}
>
<Text>Custom border</Text>
</Box>
| フック | 役割 | 主な返り値 / 引数 |
|---|---|---|
useInput(handler, opts?) |
キーボード入力ハンドラ登録 | handler(input: string, key: Key)。key.leftArrow / key.ctrl / key.return 等。opts: isActive |
usePaste(handler, opts?) |
ペースト(bracketed paste)処理 | handler(text: string)。useInput と別チャンネルで共存可 |
useApp() |
アプリのライフサイクル制御 | exit(errorOrResult?) waitUntilRenderFlush() |
useStdin() |
stdin ストリームへのアクセス | stdin isRawModeSupported setRawMode(bool) |
useStdout() |
stdout ストリームへのアクセス | stdout write(data) |
useStderr() |
stderr ストリームへのアクセス | stderr write(data) |
useWindowSize() |
ターミナルサイズ取得(リサイズ時に再レンダー) | columns: number rows: number |
useFocus(opts?) |
フォーカス状態の取得(Tab で移動) | isFocused: boolean。opts: autoFocus isActive id |
useFocusManager() |
フォーカス管理の制御 | enableFocus() disableFocus() focusNext() focusPrevious() focus(id) activeId |
useCursor() |
カーソル位置制御(IME サポート向け) | setCursorPosition({x, y} | undefined) |
useBoxMetrics(ref) |
Box 要素のレイアウト寸法取得 | width height left top hasMeasured |
import {useInput, useApp} from 'ink';
const MyApp = () => {
const {exit} = useApp();
useInput((input, key) => {
if (input === 'q') exit();
if (key.leftArrow) { /* 左移動 */ }
if (key.ctrl && input === 'c') exit();
});
return <Text>Press q to quit</Text>;
};
コンポーネントをマウントしてターミナルに出力する。Instance オブジェクトを返す。
import {render} from 'ink';
const {rerender, unmount, waitUntilExit} = render(<MyApp />);
rerender(<MyApp count={1} />); // props 更新
await waitUntilExit(); // 終了まで待機
| オプション | 型 / デフォルト | 説明 |
|---|---|---|
stdout | Writable / process.stdout | 出力先ストリーム |
stdin | Readable / process.stdin | 入力ストリーム |
exitOnCtrlC | boolean / true | Ctrl+C でアプリを終了するか |
patchConsole | boolean / true | console.log を Ink 出力と混ざらないようにパッチ |
maxFps | number / 30 | 最大フレームレート(fps) |
incrementalRendering | boolean / false | 変更行のみ再描画してちらつきを軽減 |
concurrent | boolean / false | React Concurrent モード(Suspense・useTransition が有効) |
alternateScreen | boolean / false | ターミナルの alternate screen バッファで起動(終了時に元の表示を復元) |
interactive | boolean / auto | インタラクティブモード。非 TTY / CI では自動で false |
kittyKeyboard | object / undefined | kitty keyboard protocol。修飾キー詳細・press/repeat/release イベントを取得 |
isScreenReaderEnabled | boolean / env | スクリーンリーダーモード(INK_SCREEN_READER=true でも有効) |
debug | boolean / false | 各更新を上書きせず新規出力として表示 |
onRender | ({renderTime}) => void | 各レンダー完了後のコールバック |
| メソッド | 説明 |
|---|---|
rerender(tree) | ルートノードを新しいツリーで置き換え、または現在の props を更新 |
unmount() | アプリを手動でアンマウント |
waitUntilExit() | アンマウントまで待機する Promise。exit(value) で resolve、exit(error) で reject |
waitUntilRenderFlush() | 保留中のレンダー出力が stdout にフラッシュされるまで待機 |
cleanup() | アンマウントして内部インスタンスを解放(テスト向け) |
clear() | ターミナル出力をクリア |
コンポーネントを文字列に同期レンダーする。stdout に書き出さないため、ドキュメント生成・テスト・ファイル出力に使用。columns(デフォルト: 80)でテキスト折り返し幅を指定。
import {renderToString, Text, Box} from 'ink';
const output = renderToString(
<Box padding={1}>
<Text color="green">Hello World</Text>
</Box>,
{columns: 40}
);
// → '\n Hello World \n'
ink-testing-library でコンポーネントをヘッドレスレンダーし、lastFrame() で出力文字列を検証できる。
import React from 'react';
import {Text} from 'ink';
import {render} from 'ink-testing-library';
const Hello = ({name}) => <Text>Hello, {name}!</Text>;
const {lastFrame} = render(<Hello name="World" />);
lastFrame() === 'Hello, World!'; // => true
| 機能 | 詳細 |
|---|---|
| CI モード | 環境変数 CI が設定されると、終了時に最後のフレームのみ出力(ANSI 上書きシーケンスを使用しない)。CI=false で無効化可。 |
| React Devtools | オプション依存として react-devtools-core をインストール後、DEV=true my-cli で起動し npx react-devtools で接続。コンポーネントツリーのインスペクト・props のライブ変更が可能。 |
| スクリーンリーダー | <Box aria-role="…" aria-label="…" aria-hidden> で ARIA 情報を付与。render(…, {isScreenReaderEnabled: true}) または環境変数 INK_SCREEN_READER=true で有効化。 |
| kitty keyboard protocol | mode: 'auto' で対応ターミナルを自動検出。Super / Hyper / CapsLock・press / repeat / release イベントが使用可能。 |
| Incremental Rendering | incrementalRendering: true で変更された行のみ再描画。高頻度更新 UI のパフォーマンスを大幅に改善。 |
| Alternate Screen | alternateScreen: true でターミナルの alt バッファを使用(vim / htop 方式)。終了時に以前の表示を復元。 |
useInput のリスニング・タイマー・Pending Promise などがプロセスを継続させる。useApp().exit() を呼ぶと waitUntilExit() が解決して終了する。