みなさん、pbcopy コマンドを使っていますか?
pbcopy コマンドは macOS の標準コマンドで、標準入力に与えた内容をクリップボードにコピーします。
たとえば、編集中のコードの差分をクリップボードにコピーしたい場合は、次のようにコマンドを呼び出します。
$ git diff | pbcopy
slack やメール、コードレビューのコメント欄など、様々な場所にコマンドの実行結果などを貼り付けたいときに、pbcopy コマンドは役に立ちます。ログの検索結果や grep の出力など、コマンドの出力結果を手早くコピーするときには便利です。
また、pbcopy と対になるコマンドに pbpaste があります。こちらはクリップボードの内容を標準出力に出力するものです。
ちなみに、両コマンドの “pb” という接頭語は pasteboard の略だそうです。macOS ではクリップボードのことを内部的に pasteboard と呼んでいるそうです。
さて、そんな pbcopy コマンドですが、最近の開発では利用しづらいツールとなっています。
dev containers の登場により、ローカル開発であってもコンテナの中で生活することが多くなっています。
VSCode のターミナルから pbcopy コマンドを呼び出しても command not found と言われてしまいます。
コンテナでは Linux が動作しているので pbcopy コマンドは存在していませんし、また、コンテナ内のクリップボードとホスト側のクリップボードは連動していません。僕自身コンテナ生活を始めてもう 2年近く経ちますが、その間はクリップボードは使えないものだと諦めて過ごしていました。
実際のところ、コマンドラインからクリップボードにコピーしたくなることは少なめですし、そこまで困っていなかったのですが、つい最近、久しぶりに command not found と怒られてしまったので、2024年のクリップボード環境を見直すことにしたのでした。
そんなこんなで開発したのが rpbcopyd というツールです。
rpbcopyd は remote pbcopy daemon の略で、その名の通りリモートから pbcopy を行うツールです。
rpbcopyd は以下の 2種類のコマンドから構成されています。
rpbcopyd
)rpbcopy
とペーストを行う rpbpaste
の 2つのコマンドそれぞれのコマンドは以下のように利用します。
# サーバ側では rpbcopyd を起動
$ rpbcopyd -d
# クライアント側からクリップボードにテキストを転送
$ git diff | rpbcopy
# クリップボードから値を読み取る
$ rpbpaste | grep hello
特別な設定などなく、呼び出すだけで起動して利用できるような構造になっています。
ちなみに、rpbcopyd は自動的に起動するように macOS の設定をしておくと便利です。
簡単に rpbcopyd / rpbcopy がどうやって動いているのかをご紹介します。
これらのコマンドは、Docker Desktop の提供する host.docker.internal
という特殊なホストを利用して通信しています。Docker を起動するだけで自動的に有効になるため、特別な設定は必要ありません。
実際の通信は HTTP の GET と POST を利用しています。
もう少し原始的なプロトコルを使っても良かったのですが、実装する際に Web フレームワークや HTTP クライアントライブラリが利用できるので、かんたんに実装ができるという理由から HTTP を採用しました。
HTTP を利用しているので、実は curl コマンドでも rpbcopyd にアクセスすることができます。
$ curl -X POST http://host.docker.internal:12345/ -d "Hello rpbcopyd"
なお、rpbcopyd は Rust で実装されています。自分の初 Rust プログラムです。
実は rpbcopyd を作り始める前に、 macOSとLinux devcontainer間のシームレスなクリップボード共有 #Linux という記事を見かけていました。この記事では SSH のリモートポートフォーワーディングを利用してクリップボードを共有しています。
すでに完成している方法だったのですが、devcontainer を起動するたびに SSH を立ち上げる必要があるので、ずぼらな自分には合わないと実装の道を選びました。
今になって考えると、この手法のコア部分は socat と netcat による通信だったので、host.docker.internal を使うようにアレンジすればすべて解決でした。わざわざ車輪の再発明をしてしまったかもしれません。
今回は host.docker.internal を使う方法を採用しましたが、名前付きパイプを使う案も検討していました。
手元の環境 bind mount を撤廃している都合から不採用としたのですが、このアプローチを採用した場合は WSL内のdockerコンテナからWindowsのクリップボードにコピーする や glidenote/rpbcopy のような実装になったものと思われます (実装後に見つけました)。
他にも Cross platform clipboard | networkless! remote copy や remote pbcopy over ssh といった実装が存在しており、多くの人たちがこの課題に取り組んでいるようです。
macOS と dev containers 間でクリップボードを共有する方法を紹介しました。僕は rpbcopyd というツールを開発しましたが、他にもいくつかのアプローチがあるので良さそうなものを選ぶとよいでしょう。
なお、このツールを作る中で、Rust を学んでみたのはいい勉強になりました。プロトタイプは Ruby で書いたのですが、せっかくなので新しいチャレンジをしてみました。なにかの機会があればまた Rust でなにか作りたいですね。