PROJECT PAIRWIN · HAMMERSPOON

視窗配對彈跳

⌥Z 兩下,把任何兩個視窗拉到眼前

在 macOS 26 封死所有「跨桌面搬視窗」API 之後,用實測繞出活路

2026 · 08 · 12 — 一日完成 · MACOS 26.5 × HAMMERSPOON

2
下按鍵 —— 跨任意桌面,拉攏任何兩個視窗
最多 4 下2 下・ 同桌面瞬間・重開約 1 秒・真搬窗約 5

按第一下標記,按第二下自動選最快路徑併排眼前——成功後自動歸零,可立即配下一組

CONTENTS · 目錄

七章,一條偵探故事線

壹 · 背景與目標

問題:11 個桌面裡,人會迷路

工作機上開著 11 個虛擬桌面。想把 A 桌面的視窗和 B 桌面的視窗湊在一起對照時,切到第二個 app 的瞬間,第一個視窗在哪個桌面的記憶就斷了——來回找窗的時間比對照本身還久。

現況一

快速切換鍵

常用 app 都設了快捷鍵,切 app 很快——但切過去之後,人就離開了原地。

現況二

視窗排版鍵

左右併排一鍵完成——但只對「已經在同一桌面」的視窗有效。

缺口

跨桌面那一環

兩套工具都終結在桌面邊界。跨 Space 的「拉攏」沒有任何現成方案。

壹 · 背景與目標

目標:不是人去找視窗,是視窗來找人

最初的想法是「畫面遮罩高亮」幫人記路。但高亮只能標出視窗在哪,人還是得跳過去、還是得手動搬。真正的解法是反轉方向:

否決

幫人記路

Mission Control 疊高亮、HUD 標位置——治標。省下的只有「想起來在哪」,搬運照舊。

採用

把視窗叫過來

一組按鍵 = 目標視窗自己出現在眼前、自動併排。人從頭到尾不離開現場,就不存在迷路。

設計原則:省的不是「找」的時間,是把「找」這件事整個消滅

貳 · 絕境與活路

絕境:macOS 26 把「搬窗」的路全封了

逐項本機實測(macOS 26.5.2 / Hammerspoon 1.1.1),三條傳統路徑全數陣亡

官方搬窗 API

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 隔離,抓得到別桌面視窗的即時畫面——「鏡像」的基礎

Mission Control 拖縮圖

MC thumbnail drag + AX(Dock)

桌面上拖窗被封死,但 MC 裡拖縮圖到空間列還活著——真搬窗的最後一條通道。

破案關鍵:封鎖是「按通道」不是「按功能」——換一條 WindowServer 通道,搬窗就復活

參 · 解法

最終形態:⌥Z 兩下拉攏

  1. 第 1 下——在第一個視窗按:標記 🅰,右下角 HUD 亮起。
  2. 第 2 下——切到第二個視窗按:程式自動選最快路徑,把 🅰 拉到目前桌面、左右併排。
  3. 成功後自動歸零——配對清除、HUD 熄滅,立刻可以標下一組。⌥⇧Z 只剩中途反悔用;閒置 30 秒未接第二下,自動視同反悔。

歸屬規則:拉攏永遠落在「按第 2 下時人所在的桌面」——🅱 是錨、🅰 被搬。全程靜默,只有錯誤才彈提示

參 · 解法

第 2 下按下去的三叉決策

程式依 🅰 的狀態自動選路,使用者不需要知道也不需要選:

瞬間

同桌面

🅰 就在眼前這個桌面 → 直接左右併排,不切桌面、不開新窗。

約 1 秒
🪟

就地重開

🅰 是 Finder/Chrome/Safari → 在本桌面開同內容新視窗(同資料夾/同網址),標記自動改指新窗。

約 5 秒
🚚

真搬窗

🅰 開不了第二個視窗(Claude、ChatGPT、Word…)→ 經 Mission Control 把本尊搬過來。失敗再退回唯讀鏡像。

SCROLL —— 三條路徑依序點亮

肆 · 核心技術

真搬窗 mcMoveHere():六步偷渡

桌面上拖窗被拒收,但 Mission Control 裡「拖縮圖到空間列」是另一條 WindowServer 通道——而 MC 的介面屬於 Dock 程序,輔助功能(AX)讀得到每一顆按鈕的精準座標

  1. 切到 🅰 所在桌面 gotoSpace,取得視窗即時標題
  2. 開 Mission Control toggleMissionControl()(合成 ⌃↑ 無效,實測踩雷)
  3. AX 掃 Dock:拿到視窗縮圖鈕+「桌面 N」空間列鈕的座標
  4. 合成拖曳抓住縮圖,拖向頂部空間列
  5. 放到目標桌面鈕上 → 視窗本尊飛過去
  6. 關 MC、回家 gotoSpace(home) → 視窗已在本桌面,併排、聚焦
肆 · 核心技術

最貴的一個坑:空間列會「展開」

第一次實作精準落點失敗——瞄準桌面 2,落在桌面 3。診斷發現:拖曳懸停會讓空間列從文字膠囊展開成大縮圖,所有按鈕座標整段位移,拖曳前抄的座標全部作廢。

失敗版

拖到一半才查座標

拖曳中重查 AX——時機不可靠,落點差一格。視窗被搬去錯的桌面。

修正版

先懸停、抄座標、再拖

拖之前先用游標懸停讓空間列展開 → 抄下「展開後」的目標鈕座標 → 抓縮圖直拖過去。拖曳懸停會重現同一套展開幾何,座標精準命中

肆 · 核心技術

保險絲:即時鏡像

真搬窗萬一失敗(Mission Control 抽風、縮圖找不到),不能讓使用者摔在地上。退路是用跨桌面截圖做一面即時鏡像

每秒 2 次更新

snapshotForID 連拍別桌面的視窗畫面,貼在半屏面板上——對照、抄資料完全夠用。

唯讀・可點

鏡像不能操作,但點一下就跳過去本尊視窗——看與操作各走各的最短路。

標記保留

失敗狀態下標記不清除,再按 ⌥Z 重試搬運(逾時規則同前,詳下頁)。

層層退路:就地重開 → 真搬窗 → 鏡像 → 跳過去。任何一層失敗都有下一層接住,永遠不迷路

肆 · 核心技術

防呆:把「狀態」關進籠子

30 秒逾時歸零

標了 🅰 後放著不管?30 秒自動視同反悔清除——不會有殭屍標記過夜。倒數在第 2 下按下時停止、失敗後重啟。

源自審查

開機世代戳

視窗 ID 每次開機重編。標記檔連開機時間戳一起存,重開機後舊標記直接作廢——防 ID 被新視窗重用而認錯人。

全程靜默

只有錯誤才彈提示,進度看右下角跨桌面 HUD。唯一例外:搬運中「勿動滑鼠」警告——合成拖曳期間人手一動就會打架。

源自審查

互斥與吞鍵

重開輪詢在飛時擋掉連按;真搬窗結束後 1 秒內吞掉排隊按鍵——連打 ⌥Z 不會開出重複視窗

伍 · 品質工程

寫完不是結束:15 個代理的對抗式審查

首版程式完成後,交給多代理審查流水線:3 個視角(Lua 正確性/狀態機邏輯/系統整合)並行找碴,每一項發現再交給獨立的「懷疑者」代理專職推翻——推不翻的才算真缺陷。

51

工具呼叫
逐行檢證、實際讀碼

81 萬 tokens

投入算力
全程約 12 分鐘

9

確認缺陷
戰果詳下頁

審查對象為首版狀態機;其後新增的鏡像與真搬窗程式碼,由 5 項實測與瘦身迴歸把關(見成效章)

伍 · 品質工程

戰果:9 項確認缺陷,全數修復

Major(會咬人) Minor(邊角案例)
32 10 Lua 正確性視角:Major 1 項 Lua 正確性視角:Minor 2 項 狀態機邏輯視角:Major 2 項 狀態機邏輯視角:Minor 1 項 系統整合視角:Major 1 項 系統整合視角:Minor 2 項 12 21 12 共 3 項 共 3 項 共 3 項 Lua 正確性 狀態機邏輯 系統整合
看這裡:三個視角戰果平均、無一掛零——多視角互補的佐證。合計 4 Major+5 Minor,全數修復。
DATA TABLE ▾
視角MajorMinor
Lua 正確性12
狀態機邏輯21
系統整合12
伍 · 品質工程

四個 Major:每一個都會真的咬人

MAJOR 01

Finder 腳本順序反了

activate 寫在 make new 之前——系統會先切去舊桌面,新視窗開錯地方、人被硬拉走,與功能目的完全相反。

對調順序:先 make new(落在當前桌面)再 activate。

MAJOR 02

連按重入競態

重開等待期間再按 ⌥Z,AppleScript 重複執行——開出重複視窗+孤兒視窗。彈跳鍵被連按是常態操作,觸發率高。

流程在飛時直接吞鍵;輪詢計時器作為互斥鎖。

MAJOR 03

清除後幽靈復活

⌥⇧Z 清除沒取消在飛的輪詢——輪詢回頭把清掉的標記寫回來,產生 HUD 看不見的幽靈標記,之後跳去使用者以為早已清除的視窗。

清除先掐計時器;寫回前驗標記身分。

MAJOR 04

載入清掃誤殺

重載時探測不到的標記一律當死標記刪——但最小化+在別桌面的活視窗也探測不到,改個設定檔就無聲丟配對。

改用開機時間戳判世代:同世代保留、跨世代作廢。

陸 · 成效與數據

實測:5 項全過

亮點頁的三個速度數字(瞬間/約 1 秒/約 5 秒)即出自上列實測。真搬窗測試當下使用者人在第 8 個桌面,落點仍精準命中

陸 · 成效與數據

往簡潔收斂:行為不變,程式變少

瘦身前瘦身後 瘦身前:789 行(含已無用的舊三段流程) 瘦身後:700 行(刪除死路徑,行為不變) 789 行 700 行 −89 行:整段刪除永遠跑不到的舊三段流程
互動也同步收斂:拉攏所需按鍵 最多 4 下2 下;提示訊息全撤、只留錯誤警告。

簡潔不是一開始的目標,是功能成熟後的第二輪工程:確認兩下拉攏是唯一主流程後,舊路徑(彈跳/召喚/鏡像切換)整段退役。

柒 · 教訓

事故誠實記錄:測試工具自己出了車禍

開發途中一度所有指令掛死——調查後確認不是功能的問題,是測試手法的鍋

事故

強殺等待中的指令通道

用 timeout 硬殺等待回應的 CLI 客戶端,弄壞了 Hammerspoon 的 IPC 通訊埠——之後所有指令掛死或洪水報錯,重啟才救回。

教訓

驗證改走檔案

長測試改為「結果寫檔、外部讀檔」,不依賴會斷的回傳通道;並確立「絕不中途強殺」的測試紀律。熱鍵本身不走這條通道,使用者完全無感

另兩筆誠實帳:一次桌面控制授權遭拒(後來證明根本不需要那個權限);螢幕錄製定期再授權恰好被鏡像功能觸發,授權後持續可用

柒 · 迭代軌跡

同一天,八個版本

柒 · 下一步

已知邊界,與留給下一版的路

觀察中

重力井現象

拉攏永遠落在按第 2 下的桌面——常用桌面會「只進不出」越堆越多。使用者已知悉並拍板維持現狀;若成困擾,候選解法:「送回去」手勢、反向錨定。

已知限制

中文語系綁定

空間列按鈕以「桌面N」文字辨識,系統改英文語系需同步改判斷式(檔內已註記位置)。

可調參數

檔頂常數

逾時秒數、新窗靠左/靠右、HUD 開關、鏡像更新頻率——全部集中在檔案頂部,不用讀懂程式就能改。

EPILOGUE

系統封了所有的門,
我們就從 Mission Control 的窗戶把視窗搬回家。

一天之內,從「切過去就迷路」到「⌥Z 兩下、自動歸零」。
實測驅動、對抗審查、誠實記帳——這是一份可以被檢驗的工程。

鈔於 2026 · 08 · 12 — PAIRWIN v1.0 · 700 LINES OF LUA
HAND-BOUND IN HAMMERSPOON · 一日而成