截圖自己跨到另一台 Mac
我平常人坐在 MacBook Air 前面,Claude Code 跑在另一台 MacBook Pro 上,用 mosh(耐斷線版的 ssh,換了網路、筆電闔上再打開,連線都還在)連過去。在 Air 上截一張圖,切回終端機按 Ctrl+V——沒反應。
原因不難猜:Claude Code 是跑在 MBP 上的程式,按 Ctrl+V 的時候它去讀的是 MBP 自己的剪貼簿。圖片在 Air 的剪貼簿裡,兩台各有各的,它當然找不到。
clipwatch 就是為了這件事寫的:一隻常駐在背景的小程式,盯著 Air 的剪貼簿,只要出現圖片就先幫我搬到 MBP 上。等我按 Ctrl+V 的時候,圖片已經在那邊等著了。
這篇分享:它實際上在做什麼、兩台各裝什麼、以及誰會需要、誰不需要。
它做的事只有一件
整件事就三個步驟:
- 在 Air 上每 0.25 秒看一次剪貼簿有沒有變
- 變了,而且新內容是圖片 → 轉成 PNG
- 用 ssh 把這張圖餵給 MBP 上的一支腳本,那支腳本把它放進 MBP 的剪貼簿
沒有伺服器、沒有帳號、沒有雲端。整條路就是一條 ssh 連線,走我本來就有的 Tailscale(把自己的幾台機器連成一個私有網路,不用對外開連接埠),圖片不會經過任何第三方。
Air 那端是一份 105 行的 Swift 檔(clipwatch.swift),編譯成一個 app 放在 ~/Applications/,交給 launchd(macOS 內建管背景程式的機制)開機自動跑,掛掉也會自動拉回來。主迴圈先不管待會要講的例外狀況,骨架是這樣:
while true {
let change = pb.changeCount
if change != lastChange {
lastChange = change
if let png = clipboardPNG() {
push(png) // 用 ssh 送到另一台
}
}
usleep(pollInterval) // 250_000 微秒 = 4 Hz
}
為什麼是每 0.25 秒問一次,而不是等通知
這點我原本以為有更漂亮的做法。macOS 的 NSPasteboard(系統剪貼簿那層介面)沒有「內容變了」的通知可以訂閱——系統不會主動告訴你。它給的只有 changeCount 這個數字,每次有人寫剪貼簿它就加一。所以唯一的辦法是自己定期去看那個數字有沒有跳。
4 Hz 是我自己選的:截完圖、切視窗、按 Ctrl+V,這中間至少一兩秒,0.25 秒的延遲感覺不出來,再快也只是多耗電。
另外一個細節是讀取方式。程式先用 availableType(from:) 問剪貼簿「你宣告自己有哪些格式」,確認是圖片才去拿內容。所以我複製一段密碼、一段文字的時候,這隻程式從頭到尾沒有碰過那些內容。
MBP 那端只有 17 行
收的那端更簡單,一支叫 clipset 的 zsh 腳本:
#!/bin/zsh
emulate -L zsh
set -euo pipefail
dir="$HOME/.cache/clipsync"
img="$dir/in.png"
mkdir -p "$dir"
cat > "$img"
[[ -s "$img" ]] || { print -u2 "clipset: empty input"; exit 1; }
LC_ALL=en_US.UTF-8 /usr/bin/osascript \
-e "set the clipboard to (read (POSIX file \"$img\") as «class PNGf»)"
把 stdin 進來的 PNG 寫成檔案,再用 osascript 叫 macOS 把它放進剪貼簿。«class PNGf» 是 AppleScript 指定 PNG 格式的寫法,它必須以 UTF-8 送進去,locale 不對,«» 這兩個符號就會變成亂碼——所以前面那行 LC_ALL 不是可有可無。
為什麼是兩隻程式,不是一隻
兩台機器之間我最常搬的其實是文字——一段指令、一個路徑、一行錯誤訊息——而且是雙向的。這件事有現成的工具,我跑的是 p2p-clipboard(開源、MIT、跨平台,走 libp2p 的點對點同步,中間沒有伺服器)。有人做好了就不用自己寫。
它做不到的是圖片。README 寫得很直接:只支援純文字(Only supports pure text contents),而且網路封包上限是壓縮後 64KB,換算原始資料大約 150KB。一張截圖動輒好幾百 KB,這兩個條件都不符合。
clipwatch 補的就是這個洞。文字歸 p2p-clipboard,圖片歸 clipwatch,而且兩隻不能重疊——同時寫同一個剪貼簿會互相蓋掉,所以 clipwatch 一看到文字就跳過。
而且只做單向
Air → MBP,反過來不會動。因為我的需求本來就是單向的:我在 Air 上截圖,工具在 MBP 上跑。反過來我根本用不到,就沒寫。
螢幕共享的時候要讓位
這段是實際用了一陣子才踩到的。macOS 內建的螢幕共享(Screen Sharing)自己會同步剪貼簿,而且是雙向的。它在同步、clipwatch 也在寫,兩邊搶同一個剪貼簿,結果是我剛複製的東西被蓋掉。
第一版做得太暴力:只要偵測到有螢幕共享連線,就整個停掉。結果變成我開著螢幕共享的時候截圖,圖片永遠到不了,Ctrl+V 又失效了。
現在的判斷只看一件事:螢幕共享的視窗是不是在最前面。
func shouldYieldToScreenSharing() -> Bool {
return NSWorkspace.shared.frontmostApplication?.bundleIdentifier
== "com.apple.ScreenSharing"
}
我人在螢幕共享視窗裡面操作,那時候該同步剪貼簿的是它,clipwatch 讓位;我人在終端機,clipwatch 照樣搬圖。
什麼情況下它會派上用場
- 你有兩台 Mac,一台在你面前、一台在跑東西,而且你會 ssh 或 mosh 過去
- 你常常在 A 截圖,但要貼進跑在 B 上的工具——Claude Code、Codex 這類活在終端機裡的 AI
- 你想要它在背景自己完成,不想每次都多一個手動步驟
- 你已經有 Tailscale,或任何一條不用打密碼的 ssh 路
但有幾種情況裝了也沒用
- 只有一台 Mac:用不到
- 想同步文字:這隻不管文字,要另外找工具
- 想要雙向:目前只有單向,要反過來得自己再寫一份
- 偶爾傳一兩張圖:AirDrop 或 Apple 的通用剪貼板(Universal Clipboard)就夠了。它的條件是兩台靠得夠近、登入同一個 Apple 帳號、藍牙和 Wi-Fi 都開著、Handoff 打開,而且內容只留大約兩分鐘——它設計上是「兩台放在旁邊」的功能,不是跨網路的常駐同步
- 不想自己編譯:這隻沒有安裝檔。要用得自己把那份 Swift 編譯起來,再寫一份 launchd 設定檔
它沒有圖示、沒有設定畫面,我按 Ctrl+V 的時候也不會想到它。
p2p-clipboard(文字同步那隻):https://github.com/gnattu/p2p-clipboard Apple 通用剪貼板的使用條件:https://support.apple.com/zh-tw/102430