package main import ( "context" "embed" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" wruntime "github.com/wailsapp/wails/v2/pkg/runtime" ) //go:embed all:frontend/dist var assets embed.FS //go:embed build/trayicon.png var trayIcon []byte type wailsCtx = context.Context // 版本號由 build 時 -ldflags 注入(同 arcrun-tray 的規矩)。 var version = "dev" func main() { app := NewApp() // 🔴 托盤必須在**主執行緒**建立(NSStatusItem 內部會 new NSWindow, // AppKit 規定 NSWindow 只能在主執行緒 new)。 // 踩過的兩個錯(都真機實測炸掉): // ① systray.Run() 自己開 loop ⇒ 與 Wails 搶主 loop ⇒ SIGTRAP // ② 在 OnStartup 或 goroutine 裡呼叫 ⇒ 「NSWindow should only be // instantiated on the main thread!」⇒ SIGABRT // 正解:main() 一開始(還在主執行緒)就 RunWithExternalLoop 註冊並 start, // 它不阻塞,接著再把主執行緒交給 wails.Run()。 // 🔴 托盤必須在**主執行緒**建立(NSStatusItem 內部會 new NSWindow)。 // 改用原生 NSStatusItem(見 tray_darwin.m)——實測證明 energye/systray // 在 RunWithExternalLoop 之下**根本不會建立托盤**(onReady 從未觸發)。 setupTray(app) installSignalHandler() // 收到 TERM/INT 要能正常退出(不必強制結束) err := wails.Run(&options.App{ Title: "Arcrun", // 這裡的 Width/Height 只是**開得起來的保底值**;真正的尺寸在 OnStartup // 依螢幕算成 50%(見下方 resizeToHalfScreen)。保底值不能大於常見筆電 // 的可視高度,否則 ScreenGetAll 失敗時會沿用它而擠出畫面外 // (leo 08-06 實撞:1000x700 在他的 Windows 上「高度太高快擠到外面了」)。 Width: 960, Height: 600, MinWidth: 820, MinHeight: 560, AssetServer: &assetserver.Options{Assets: assets}, // 🔴 2026-08-06:沒有這段,Arcrun.exe **點幾次就開幾個實例**, // 每個實例各掛一個系統匣 icon(leo 的封測者開到 4 個,分不清該點哪個), // 而且每個實例還各自拉一份 collector 子行程。 // macOS 由 LaunchServices 天然單實例 ⇒ 這個病**只在 Windows 現形**。 // 第二次啟動=叫醒既有視窗後自己退出(Wails 內部 os.Exit(0))。 SingleInstanceLock: &options.SingleInstanceLock{ UniqueId: "dev.arcrun.rag.daemon", OnSecondInstanceLaunch: func(options.SecondInstanceData) { app.ShowWindow() }, }, OnStartup: func(ctx wailsCtx) { app.startup(ctx) resizeToHalfScreen(ctx) // 視窗=螢幕的一半,置中(leo 08-06) startSupervisor() // 🔴 daemon 本體:不啟動就不會同步 // 🔴 托盤必須在**主執行緒**建立(NSStatusItem 會 new NSWindow)。 // OnStartup 不是主執行緒 ⇒ 直接呼叫會炸 // 「NSWindow should only be instantiated on the main thread!」(實測)。 // 用 Wails 的 dispatch 把它丟回主執行緒。 }, // 🔴 daemon 是常駐程式:關窗只是收起來,**不能真的結束** // (結束=停止看守資料夾,使用者不會預期按 × 就不再同步)。 OnBeforeClose: func(ctx wailsCtx) bool { wruntime.WindowHide(ctx) return true // true=攔下關閉 }, HideWindowOnClose: true, // Dock 不出現 icon:Wails v2.13 的 mac.ActivationPolicy 還沒開放(原始碼裡是註解掉的), // 改走 Info.plist 的 LSUIElement(與 fyne 版同一個做法,見 build/darwin/Info.plist)。 Bind: []interface{}{app}, }) if err != nil { println("Error:", err.Error()) } } // resizeToHalfScreen 把視窗調成螢幕長寬的一半並置中。 // // 🔴 leo 2026-08-06(Windows 封測,圖三標註): // // 「視窗尺寸應該是 default 50% 螢幕長寬,**高度太高快擠到外面了**」 // // 寫死 1000x700 在 1366x768 這種常見筆電上,扣掉工作列就幾乎頂到底。 // 依螢幕算才不會在別人機器上爆版——我們沒有全世界的螢幕可以試。 // // 拿不到螢幕資訊時**什麼都不做**(沿用 options 的保底值),不要瞎猜一個數字: // 猜錯的後果就是現在這個病。 func resizeToHalfScreen(ctx wailsCtx) { screens, err := wruntime.ScreenGetAll(ctx) if err != nil || len(screens) == 0 { return } // 優先用「視窗現在所在的那面」;沒有就退回主螢幕,再退回第一面。 s := screens[0] for _, c := range screens { if c.IsCurrent { s = c break } if c.IsPrimary { s = c } } // Size 是邏輯像素(Wails 設定尺寸用的單位);舊欄位 Width/Height 已 deprecated, // 但某些平台只填舊欄位 ⇒ 兩個都試,取得到值的那個。 w, h := s.Size.Width, s.Size.Height if w == 0 || h == 0 { w, h = s.Width, s.Height } if w == 0 || h == 0 { return } wruntime.WindowSetSize(ctx, w/2, h/2) wruntime.WindowCenter(ctx) } // setupTray 建立選單列 icon。 // // 🔴 leo 2026-08-05:「依照 google drive,托盤裡**只剩下按右鍵會結束**, // // 其他設定都在這個頁面,**點擊托盤的 icon 就立刻展開界面**」 // // ⇒ 左鍵=直接開視窗(不彈選單);右鍵=只有一項「結束 Arcrun」。 // // 所有設定都在視窗裡,托盤不再是第二套 UI(fyne 版把設定塞在下拉選單, // leo:「daemon 設定項越來越多,不可能統統塞在下拉選單」)。 // // 🔴 **不可以用 systray.Run() 開自己的迴圈**(我第一版這樣寫,真機一跑就 SIGTRAP): // // macOS 的選單列必須在**主執行緒**跑,Wails 已經佔著那條 loop, // 兩個 toolkit 搶主 loop ⇒ `signal arrived during cgo execution` 直接崩。 // 正解是 `RunWithExternalLoop`——它只註冊、把 start/end 交給既有的 loop 呼叫。 var trayStart, trayEnd = func() {}, func() {}