複数行を貼り付けると勝手に走る、を直す

Slack や ChatGPT から複数行コマンドを貼ると、Enter のたびに実行されて焦った経験ありませんか。あれを止める小さな規格の話。

こういう複数行をドキュメントからコピーして、ターミナルに貼りつけたい時があります:

Terminal window
cd ~/projects
git pull
cargo build --release

素朴なターミナルに貼ると、Enter のたびに順番に実行されます:

$ cd ~/projects
$ git pull
Already up to date.
$ cargo build --release
Compiling ...

rm -rf とか drop table とか混じってたら、一発で事故。

これ、ターミナル自身が昔から用意している対策があります。

ターミナルには「ペースト」という概念がない

まず驚くかもしれませんが、ターミナルに「ペースト」なんていうイベントは元々ありません

OS がブラウザやエディタから貼り付けられた文字を、1 文字ずつキー入力としてターミナルに流し込んでいるだけ。人間が超高速でタイピングしているのと区別がつかないので、改行文字はそのまま Enter キーとして解釈されて、コマンドが走ります。

Bracketed Paste: 「これ貼り付けだよ」印を付ける

Bracketed Paste Mode という仕組みがあって、これを有効にするとターミナルは貼り付けの前後に目印を付けてシェルに渡します:

\x1b[200~ cd ~/projects
git pull
cargo build --release \x1b[201~
  • \x1b[200~ → ペースト開始
  • \x1b[201~ → ペースト終了

シェル(bash / zsh)はこの目印を見たら、「間の改行は Enter じゃなくてただの文字」と解釈します。結果、貼り付けた全部がコマンドラインに並んで、ユーザーが自分で Enter を押すまで実行されない

ccmux での使い方

ccmux はターミナルの上にさらにペインを載せているので、2 段階の仕事があります。

1. ccmux 自身がペースト検知を有効化:

src/main.rs
execute!(stdout, EnableBracketedPaste)?;

これで crossterm(端末操作ライブラリ)が、貼り付けを 1 つの Event::Paste(text) イベントにまとめてくれます。

2. 内側の PTY に、印付きで転送:

src/app.rs
pub fn forward_paste_to_pty(&mut self, text: &str) -> Result<()> {
let mut data = Vec::new();
data.extend_from_slice(b"\x1b[200~");
data.extend_from_slice(text.as_bytes());
data.extend_from_slice(b"\x1b[201~");
self.focused_pane().write_input(&data)?;
Ok(())
}

中の bash は「これ貼り付けだな」と気付いてくれる。

古い環境への保険: 6 バイト閾値

EnableBracketedPaste が効かない古い環境だと、crossterm から普通の Key イベントが超高速で連続して飛んできます。

ccmux はメインループで「1ms 以内に貯まったキー入力の量」を見て、6 バイトより多ければ貼り付けと判定:

src/main.rs
if buffer.len() > 6 {
// 貼り付け判定 → ブラケット印で包んで送る
} else {
// 通常のタイピング → そのまま送る
}

人間のタイピングは速くても 50ms ごとに 1 文字。1ms に 6 文字以上は人間にはできない、という経験則。

気を付けること

  • ネストした多重化(ccmux の中で tmux)を動かすとブラケットが二重になって壊れる → だから ccmux はネスト起動を拒否している
  • 古い bash(< 4.0)/ 古い zsh(< 5.0)ではサポート外 → いまどきの環境ならまず大丈夫

「ペーストで事故る」の答えは、意外にもターミナル規格の側にもう存在していたというオチ。規格を読んで素直に使うと、大抵の UX 問題は既に解決されています。次は描画ループの話を書きます。