DAY 261

截圖自己跨到另一台 Mac

我平常人坐在 MacBook Air 前面,Claude Code 跑在另一台 MacBook Pro 上,用 mosh(耐斷線版的 ssh,換了網路、筆電闔上再打開,連線都還在)連過去。在 Air 上截一張圖,切回終端機按 Ctrl+V——沒反應。

原因不難猜:Claude Code 是跑在 MBP 上的程式,按 Ctrl+V 的時候它去讀的是 MBP 自己的剪貼簿。圖片在 Air 的剪貼簿裡,兩台各有各的,它當然找不到。

clipwatch 就是為了這件事寫的:一隻常駐在背景的小程式,盯著 Air 的剪貼簿,只要出現圖片就先幫我搬到 MBP 上。等我按 Ctrl+V 的時候,圖片已經在那邊等著了。

這篇分享:它實際上在做什麼、兩台各裝什麼、以及誰會需要、誰不需要。

它做的事只有一件

整件事就三個步驟:

  1. 在 Air 上每 0.25 秒看一次剪貼簿有沒有變
  2. 變了,而且新內容是圖片 → 轉成 PNG
  3. 用 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

延伸閱讀
看完整 261 篇 →