なぜ "文字列をそのまま表示" では足りないのか
Claude Code はスピナーを回し、プログレスバーを更新し、画面を書き換える。ターミナルの上に別のターミナルを描くとき、何が必要になるか。
ccmux は「ペインの中に Claude Code を映す」ツールです。最初、こう考えました:
Claude Code の出力を受け取って、そのまま ratatui(TUI 描画ライブラリ)の画面に流せばいいのでは?
やってみると、スピナーが縦に積み上がっていきました。
⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⠋⠙...クルクル回るはずのものが、下にどんどん伸びていく。なぜ?
ターミナルへの出力は “命令” である
Claude Code が出してる「出力」は、実はこういうバイト列です:
⠋ loading...\r⠙ loading...\r⠹ loading...\r\r はカーソルを行の先頭に戻す指示。つまり Claude Code は:
⠋ loading...と書く- カーソルを行頭に戻す
⠙ loading...で上書きする- また戻す
⠹ loading...で上書き
としている。ターミナルはその「戻して上書き」をちゃんと処理するから、同じ行でクルクル回っているように見えるわけです。
\r を知らない ccmux は、改行文字と勘違いして、毎フレーム新しい行を足してました。縦に積まれて当然です。
他にも山ほど命令がある
ターミナルへの出力は、テキストだけじゃなくて命令と混ざった DSL です:
\x1b[2K→ この行を消す\x1b[H→ カーソルを左上へ\x1b[10;5H→ カーソルを 10 行目 5 列目へ\x1b[38;5;214m→ 次の文字から 256 色パレットの 214 番の色\x1b[2J→ 画面全体をクリア
Claude Code だけじゃなく、vim も htop も less も git log も、色や位置を指定するのに全部これを使います。
単純に \x1b[... を捨てる(ANSI ストリップ)と、色は消えますが、画面クリアやカーソル位置指定も無視されるので、スピナーや @ 候補補完のオーバーレイがめちゃくちゃになります。
vt100 クレートが代わりにやってくれる
vt100 は 「ターミナルエミュレータの中身」だけを提供するライブラリ です。画面は描かないけど、「今、画面のどこに何が表示されているか」を持っています。
use vt100::Parser;
// 40 行 × 120 列、スクロールバック 10000 行let mut parser = Parser::new(40, 120, 10_000);
// PTY から読んだバイトを食わせるparser.process(b"hello\r\n\x1b[31mred\x1b[0m\n");
// 現在の画面状態を取り出すlet screen = parser.screen();let cell = screen.cell(0, 0).unwrap();println!("{} fg={:?}", cell.contents(), cell.fgcolor());parser.process() にバイトを流し込むだけで、vt100 が内部で:
- カーソルを動かす
- 色を塗る
- 画面をクリアする
- 行を消す
- スクロールする
といったことを全部やってくれる。そしてあとで screen.cell(row, col) を聞けば、**「今この位置には何の文字が、何色で表示されているか」**がわかります。
描画は「今のセル一覧を ratatui に流す」だけ
ccmux は毎フレーム、vt100 の画面状態を ratatui の cell にコピーしてるだけです。
for y in 0..area.height { for x in 0..area.width { if let Some(cell) = screen.cell(y, x) { buf.get_mut(x, y) .set_symbol(cell.contents()) .set_fg(convert_color(cell.fgcolor())) .set_bg(convert_color(cell.bgcolor())); } }}「スピナーが同じ行で回転しているように見える」のも「@ 候補が正しい位置に浮かぶ」のも、vt100 が画面を正しく再構成してくれているからです。
得たもの、払ったもの
得たもの:
- Claude Code / vim / htop / less がそのまま動く
- 色と属性(太字・下線)も正しく出る
- テキスト選択も可能
- 10,000 行のスクロールバック
払ったもの:
- 毎フレーム全セルを ratatui へコピーする小さなコスト
ただ、ratatui は「前フレームとの差分だけ描画」する設計なので、実際にターミナルに送られるバイト量はごく少ない。アイドル時の CPU はほぼ 0% です。
ターミナルの上に別のターミナルを描くには、中にミニ版のターミナルエミュレータが必要。これがこの記事の一番の学び。文字を流すだけでは足りません。