⌥Z 兩下,把任何兩個視窗拉到眼前
在 macOS 26 封死所有「跨桌面搬視窗」API 之後,用實測繞出活路
2026 · 08 · 12 — 一日完成 · MACOS 26.5 × HAMMERSPOON
按第一下標記,按第二下自動選最快路徑併排眼前——成功後自動歸零,可立即配下一組
工作機上開著 11 個虛擬桌面。想把 A 桌面的視窗和 B 桌面的視窗湊在一起對照時,切到第二個 app 的瞬間,第一個視窗在哪個桌面的記憶就斷了——來回找窗的時間比對照本身還久。
常用 app 都設了快捷鍵,切 app 很快——但切過去之後,人就離開了原地。
左右併排一鍵完成——但只對「已經在同一桌面」的視窗有效。
兩套工具都終結在桌面邊界。跨 Space 的「拉攏」沒有任何現成方案。
最初的想法是「畫面遮罩高亮」幫人記路。但高亮只能標出視窗在哪,人還是得跳過去、還是得手動搬。真正的解法是反轉方向:
Mission Control 疊高亮、HUD 標位置——治標。省下的只有「想起來在哪」,搬運照舊。
一組按鍵 = 目標視窗自己出現在眼前、自動併排。人從頭到尾不離開現場,就不存在迷路。
設計原則:省的不是「找」的時間,是把「找」這件事整個消滅
逐項本機實測(macOS 26.5.2 / Hammerspoon 1.1.1),三條傳統路徑全數陣亡:
hs.spaces.moveWindowToSpace
回報成功、視窗紋風不動——靜默失效,最陰險的一種死法。
synthetic mouseDown + drag
游標動、視窗不動。WindowServer 專門拒收合成事件的搬窗,正確手法重測仍死。
hs.window.get / AX
別的桌面的視窗物件一律拿不到——連「碰到它」的資格都沒有。
結論:任何「直接把舊視窗搬過來」的常規做法,在 macOS 26 都不存在
gotoSpace · windowsForSpace
程式可以切桌面(約 1.2 秒),且從任何桌面都列得出各桌面的視窗 ID——導航系統還在。
AppleScript make new
Finder/Chrome/Safari 可用同內容開新視窗——「就地重開」等效於搬窗。
hs.window.snapshotForID
CGWindowList 不受 Space 隔離,抓得到別桌面視窗的即時畫面——「鏡像」的基礎。
MC thumbnail drag + AX(Dock)
桌面上拖窗被封死,但 MC 裡拖縮圖到空間列還活著——真搬窗的最後一條通道。
破案關鍵:封鎖是「按通道」不是「按功能」——換一條 WindowServer 通道,搬窗就復活
歸屬規則:拉攏永遠落在「按第 2 下時人所在的桌面」——🅱 是錨、🅰 被搬。全程靜默,只有錯誤才彈提示
程式依 🅰 的狀態自動選路,使用者不需要知道也不需要選:
🅰 就在眼前這個桌面 → 直接左右併排,不切桌面、不開新窗。
🅰 是 Finder/Chrome/Safari → 在本桌面開同內容新視窗(同資料夾/同網址),標記自動改指新窗。
🅰 開不了第二個視窗(Claude、ChatGPT、Word…)→ 經 Mission Control 把本尊搬過來。失敗再退回唯讀鏡像。
SCROLL —— 三條路徑依序點亮
桌面上拖窗被拒收,但 Mission Control 裡「拖縮圖到空間列」是另一條 WindowServer 通道——而 MC 的介面屬於 Dock 程序,輔助功能(AX)讀得到每一顆按鈕的精準座標:
第一次實作精準落點失敗——瞄準桌面 2,落在桌面 3。診斷發現:拖曳懸停會讓空間列從文字膠囊展開成大縮圖,所有按鈕座標整段位移,拖曳前抄的座標全部作廢。
拖曳中重查 AX——時機不可靠,落點差一格。視窗被搬去錯的桌面。
拖之前先用游標懸停讓空間列展開 → 抄下「展開後」的目標鈕座標 → 抓縮圖直拖過去。拖曳懸停會重現同一套展開幾何,座標精準命中。
真搬窗萬一失敗(Mission Control 抽風、縮圖找不到),不能讓使用者摔在地上。退路是用跨桌面截圖做一面即時鏡像:
snapshotForID 連拍別桌面的視窗畫面,貼在半屏面板上——對照、抄資料完全夠用。
鏡像不能操作,但點一下就跳過去本尊視窗——看與操作各走各的最短路。
失敗狀態下標記不清除,再按 ⌥Z 重試搬運(逾時規則同前,詳下頁)。
層層退路:就地重開 → 真搬窗 → 鏡像 → 跳過去。任何一層失敗都有下一層接住,永遠不迷路
標了 🅰 後放著不管?30 秒自動視同反悔清除——不會有殭屍標記過夜。倒數在第 2 下按下時停止、失敗後重啟。
視窗 ID 每次開機重編。標記檔連開機時間戳一起存,重開機後舊標記直接作廢——防 ID 被新視窗重用而認錯人。
只有錯誤才彈提示,進度看右下角跨桌面 HUD。唯一例外:搬運中「勿動滑鼠」警告——合成拖曳期間人手一動就會打架。
重開輪詢在飛時擋掉連按;真搬窗結束後 1 秒內吞掉排隊按鍵——連打 ⌥Z 不會開出重複視窗。
首版程式完成後,交給多代理審查流水線:3 個視角(Lua 正確性/狀態機邏輯/系統整合)並行找碴,每一項發現再交給獨立的「懷疑者」代理專職推翻——推不翻的才算真缺陷。
工具呼叫
逐行檢證、實際讀碼
投入算力
全程約 12 分鐘
確認缺陷
戰果詳下頁
審查對象為首版狀態機;其後新增的鏡像與真搬窗程式碼,由 5 項實測與瘦身迴歸把關(見成效章)
| 視角 | Major | Minor |
|---|---|---|
| Lua 正確性 | 1 | 2 |
| 狀態機邏輯 | 2 | 1 |
| 系統整合 | 1 | 2 |
activate 寫在 make new 之前——系統會先切去舊桌面,新視窗開錯地方、人被硬拉走,與功能目的完全相反。
對調順序:先 make new(落在當前桌面)再 activate。
重開等待期間再按 ⌥Z,AppleScript 重複執行——開出重複視窗+孤兒視窗。彈跳鍵被連按是常態操作,觸發率高。
流程在飛時直接吞鍵;輪詢計時器作為互斥鎖。
⌥⇧Z 清除沒取消在飛的輪詢——輪詢回頭把清掉的標記寫回來,產生 HUD 看不見的幽靈標記,之後跳去使用者以為早已清除的視窗。
清除先掐計時器;寫回前驗標記身分。
重載時探測不到的標記一律當死標記刪——但最小化+在別桌面的活視窗也探測不到,改個設定檔就無聲丟配對。
改用開機時間戳判世代:同世代保留、跨世代作廢。
亮點頁的三個速度數字(瞬間/約 1 秒/約 5 秒)即出自上列實測。真搬窗測試當下使用者人在第 8 個桌面,落點仍精準命中
簡潔不是一開始的目標,是功能成熟後的第二輪工程:確認兩下拉攏是唯一主流程後,舊路徑(彈跳/召喚/鏡像切換)整段退役。
開發途中一度所有指令掛死——調查後確認不是功能的問題,是測試手法的鍋:
用 timeout 硬殺等待回應的 CLI 客戶端,弄壞了 Hammerspoon 的 IPC 通訊埠——之後所有指令掛死或洪水報錯,重啟才救回。
長測試改為「結果寫檔、外部讀檔」,不依賴會斷的回傳通道;並確立「絕不中途強殺」的測試紀律。熱鍵本身不走這條通道,使用者完全無感。
另兩筆誠實帳:一次桌面控制授權遭拒(後來證明根本不需要那個權限);螢幕錄製定期再授權恰好被鏡像功能觸發,授權後持續可用
否決「遮罩高亮」,定調「把視窗叫過來」。
標→標→彈跳/重開;15 代理審查修掉 9 項缺陷。
「有時可以有時不行」=可重開與不可重開的差別,不是隨機故障。
跨桌面截圖活路 → 唯讀鏡像併排。
發現 MC 拖縮圖通道還活著 → mcMoveHere(),任何 app 都搬得動。
使用者拍板:第 2 下直接整合+成功自動清除。
提示全撤只留錯誤;閒置 30 秒自動歸零。
789 → 700 行,舊流程退役,迴歸全過。
拉攏永遠落在按第 2 下的桌面——常用桌面會「只進不出」越堆越多。使用者已知悉並拍板維持現狀;若成困擾,候選解法:「送回去」手勢、反向錨定。
空間列按鈕以「桌面N」文字辨識,系統改英文語系需同步改判斷式(檔內已註記位置)。
逾時秒數、新窗靠左/靠右、HUD 開關、鏡像更新頻率——全部集中在檔案頂部,不用讀懂程式就能改。
一天之內,從「切過去就迷路」到「⌥Z 兩下、自動歸零」。
實測驅動、對抗審查、誠實記帳——這是一份可以被檢驗的工程。
鈔於 2026 · 08 · 12 — PAIRWIN v1.0 · 700 LINES OF LUA
HAND-BOUND IN HAMMERSPOON · 一日而成