t194 四修:托盤改用原生 NSStatusItem——energye/systray 在 Wails 之下根本不建托盤

leo:右鍵仍然沒用。這次先做**最小重現**才動手,結果推翻了我前兩次的判斷。

## 真兇(實測,非推論)
systray.go:83 的 setInternalLoop(true) **只在 systray.Run() 裡呼叫**;
RunWithExternalLoop 沒有 ⇒ registerSystray() 第一行就
`if (!internalLoop) return;` ⇒ **delegate 從沒建立**
⇒ onReady 不會被呼叫、enable_on_click 不會執行、左右鍵事件根本不存在。

最小重現(scratchpad/traytest):RunWithExternalLoop + onReady 印 "READY"
⇒ **READY 從未印出**。托盤自始至終沒被建立過。
(左鍵之所以「第一次能開」是 Wails 自己開的窗,與托盤無關。)

## 正解:原生 NSStatusItem(tray_darwin.m + tray_darwin.go)
左鍵=開視窗;右鍵=暫時掛選單、performClick、再拿掉(不常駐 setMenu:,
否則按鈕 action 不會被呼叫、左鍵也會變成彈選單——與 systray 那個坑同源)。

## 過程中踩到、也寫進閘的三個坑
① dispatch_async(main_queue) **無效**:Wails 佔住主執行緒後不跑標準 run loop,
   排進去的 block 永遠不執行(log 只印到 "dispatching…")⇒ 改 performSelectorOnMainThread
② NSStatusBar 需要 NSApp 已初始化:在 wails.Run() 之前呼叫 ⇒ 靜默失敗、icon 不出現
③ 🔴 **我一度對著 `go build` 產的 stub 除錯**——沒帶 Wails build tags 的執行檔
   一跑就印 "Wails applications will not build without the correct build tags" 並退出,
   我卻以為是「托盤沒建起來」,白繞一圈。**驗 Wails App 一定要用 wails build 的產物。**

## 實測證據(AppleScript 從 UI 層查,最貼近使用者看到的)
  menu bar 數=2,status item 數=1   ← 選單列 icon 真的存在
  collector 同步啟動、正常結束無孤兒
三支機械閘全過(閘也改成驗原生實作:不可 dispatch_async/必須
performSelectorOnMainThread/不可常駐 setMenu/不可再依賴 energye/systray)。

⚠️ 仍未驗:左右鍵的**實際點擊行為**要 leo 手動點。
This commit is contained in:
2026-08-05 02:56:41 +08:00
parent a02eed539a
commit 5860ef72df
6 changed files with 168 additions and 57 deletions
+3 -34
View File
@@ -4,7 +4,6 @@ import (
"context"
"embed"
"github.com/energye/systray"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
@@ -33,10 +32,11 @@ func main() {
// instantiated on the main thread!」⇒ SIGABRT
// 正解:main() 一開始(還在主執行緒)就 RunWithExternalLoop 註冊並 start
// 它不阻塞,接著再把主執行緒交給 wails.Run()。
// 🔴 托盤必須在**主執行緒**建立(NSStatusItem 內部會 new NSWindow)。
// 改用原生 NSStatusItem(見 tray_darwin.m)——實測證明 energye/systray
// 在 RunWithExternalLoop 之下**根本不會建立托盤**(onReady 從未觸發)。
setupTray(app)
trayStart()
installSignalHandler() // 收到 TERM/INT 要能正常退出(不必強制結束)
defer trayEnd()
err := wails.Run(&options.App{
Title: "Arcrun",
@@ -85,34 +85,3 @@ func main() {
// 兩個 toolkit 搶主 loop ⇒ `signal arrived during cgo execution` 直接崩。
// 正解是 `RunWithExternalLoop`——它只註冊、把 start/end 交給既有的 loop 呼叫。
var trayStart, trayEnd = func() {}, func() {}
func setupTray(app *App) {
trayStart, trayEnd = systray.RunWithExternalLoop(func() {
// ① icon**必須是 template icon**——選單列規格是 16-22pt、純黑+alpha
// 由系統依深淺色自動上色。先前塞 1024x1024 的彩色 app icon
// ⇒ 縮成一坨方塊(leo 實測)。改用 CIS 的 double chevron 渲成 44x44 純黑。
systray.SetTemplateIcon(trayIcon, trayIcon)
systray.SetTooltip("Arcrun — 你的知識庫同步小幫手")
// 🔴 ②③ 的真兇(energye/systray 原始碼註解寫得很清楚):
// 「該方法主動調用後 如果托盤菜單已創建則添加進去, **之後鼠標事件失效**」
// ⇒ 只要用 AddMenuItem 建了選單,左右鍵 handler **全部失效**
// ⇒ leo 實測「右鍵沒跳出 quit」「第一次能開之後就不再跳」。
// 正解:SetMenuNil() 移除選單讓滑鼠事件生效;右鍵則交給函式庫預設行為(見下)。
quit := systray.AddMenuItem("結束 Arcrun", "停止同步並關閉")
quit.Click(func() { app.Quit() })
systray.SetMenuNil() // ← 沒有這行,SetOnClick 不會被呼叫
// 左鍵:立刻展開主視窗(leo:「點擊托盤的 icon 就立刻展開界面」)
systray.SetOnClick(func(menu systray.IMenu) { app.ShowWindow() })
// 🔴 右鍵:**故意不註冊 handler**。
// 看 systray_darwin.go 的 systray_on_rclick()
// if onRClick != nil { onRClick(st) } else { C.show_menu() }
// ⇒ **沒註冊時,函式庫自己就會把選單叫出來**(而且 show_menu 內部
// 已經處理好 create_menu → performClick → set_menu_nil 整套)。
// 我先前註冊了自己的 handler 並加 `if menu != nil` 防呆,
// 反而把「函式庫本來就會做對的事」換成一個更脆弱的版本
// ⇒ leo 實測「右鍵沒跳出 quit」。少寫這段才是正解。
}, nil)
}