なぜ "文字列をそのまま表示" では足りないのか

Claude Code はスピナーを回し、プログレスバーを更新し、画面を書き換える。ターミナルの上に別のターミナルを描くとき、何が必要になるか。

ccmux は「ペインの中に Claude Code を映す」ツールです。最初、こう考えました:

Claude Code の出力を受け取って、そのまま ratatui(TUI 描画ライブラリ)の画面に流せばいいのでは?

やってみると、スピナーが縦に積み上がっていきました。

...

クルクル回るはずのものが、下にどんどん伸びていく。なぜ?

ターミナルへの出力は “命令” である

Claude Code が出してる「出力」は、実はこういうバイト列です:

⠋ loading...\r⠙ loading...\r⠹ loading...\r

\rカーソルを行の先頭に戻す指示。つまり Claude Code は:

  1. ⠋ loading... と書く
  2. カーソルを行頭に戻す
  3. ⠙ loading... で上書きする
  4. また戻す
  5. ⠹ 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「ターミナルエミュレータの中身」だけを提供するライブラリ です。画面は描かないけど、「今、画面のどこに何が表示されているか」を持っています。

vt100 の基本 API
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% です。


ターミナルの上に別のターミナルを描くには、中にミニ版のターミナルエミュレータが必要。これがこの記事の一番の学び。文字を流すだけでは足りません。