diff --git a/cmd/arcrun-app/app.go b/cmd/arcrun-app/app.go index f25c657..9ffcfb0 100644 --- a/cmd/arcrun-app/app.go +++ b/cmd/arcrun-app/app.go @@ -299,7 +299,11 @@ func (a *App) AddFolder(accIdx int, path string) error { } cfg.Accounts[accIdx].WatchFolders = append(cfg.Accounts[accIdx].WatchFolders, path) sort.Strings(cfg.Accounts[accIdx].WatchFolders) - return saveCfg(cfg) + if err := saveCfg(cfg); err != nil { + return err + } + restartWatch() // 立刻生效,不必等下一輪 + return nil } func (a *App) RemoveFolder(accIdx int, path string) error { @@ -317,7 +321,11 @@ func (a *App) RemoveFolder(accIdx int, path string) error { } } cfg.Accounts[accIdx].WatchFolders = keep - return saveCfg(cfg) + if err := saveCfg(cfg); err != nil { + return err + } + restartWatch() + return nil } // SetAI 存 AI 設定。 @@ -341,8 +349,25 @@ func (a *App) SetAI(useGemini bool, key string) error { cfg.Accounts[i].Extractor = engine cfg.Accounts[i].GeminiAPIKey = key } - return saveCfg(cfg) + if err := saveCfg(cfg); err != nil { + return err + } + restartWatch() + return nil } // OpenURL 用系統瀏覽器開網址(下載頁/說明文件)。 func (a *App) OpenURL(u string) { runtime.BrowserOpenURL(a.ctx, u) } + +// ── 托盤會呼叫的兩個動作(t194)── + +// ShowWindow 把主視窗叫出來並帶到前景。 +// 🔴 leo 2026-08-05:「**點擊托盤的 icon 就立刻展開界面**」 +// ⇒ 左鍵不彈選單、直接開窗(Google Drive 的行為)。 +func (a *App) ShowWindow() { + runtime.WindowShow(a.ctx) + runtime.WindowUnminimise(a.ctx) +} + +// Quit 真的結束程式(=停止看守)。只有托盤右鍵那一項會呼叫。 +func (a *App) Quit() { runtime.Quit(a.ctx) } diff --git a/cmd/arcrun-app/build-mac.sh b/cmd/arcrun-app/build-mac.sh new file mode 100755 index 0000000..0ae242e --- /dev/null +++ b/cmd/arcrun-app/build-mac.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# build-mac.sh — 打包 Arcrun.app(Wails 版,t194) +# +# 與 fyne 版最重要的差別:**要把 collector 同綑進 .app**。 +# daemon 的本體是 `collector direct`(監看/萃卡/上傳),App 只是它的門面; +# 沒同綑 ⇒ 裝了也不會同步。 +set -euo pipefail +cd "$(dirname "$0")" +VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}" +export PATH="$PATH:$(go env GOPATH)/bin" +echo "🏷 版本:${VERSION}" + +echo "① 編 collector(同綑執行檔,純 stdlib、CGO_ENABLED=0)" +( cd ../.. && CGO_ENABLED=0 go build -ldflags "-s -w" -o "cmd/arcrun-app/arcrun-collector" . ) + +echo "② wails build" +wails build -clean -ldflags "-X main.version=${VERSION}" + +APP="build/bin/Arcrun.app" +[ -d "build/bin/arcrun-app.app" ] && { rm -rf "$APP"; mv "build/bin/arcrun-app.app" "$APP"; } + +echo "③ 同綑 collector 進 .app(App 用 os.Executable() 找同層)" +cp arcrun-collector "$APP/Contents/MacOS/arcrun-collector" + +echo "④ ad-hoc 重簽(改過 bundle 內容必重簽,否則 launchd 拒開)" +codesign --force --deep --sign - "$APP" 2>/dev/null || true + +rm -f arcrun-collector +echo "✅ 完成:$APP" +echo " 驗法:open '$APP' → 選單列出現 icon、Dock **不出現** icon" +echo " 點 icon → 立刻展開視窗;右鍵 → 只有「結束 Arcrun」" diff --git a/cmd/arcrun-app/build/darwin/Info.plist b/cmd/arcrun-app/build/darwin/Info.plist index 4de2887..3d3d717 100644 --- a/cmd/arcrun-app/build/darwin/Info.plist +++ b/cmd/arcrun-app/build/darwin/Info.plist @@ -1,6 +1,10 @@ + + LSUIElement + CFBundlePackageType APPL CFBundleName diff --git a/cmd/arcrun-app/build/trayicon.ico b/cmd/arcrun-app/build/trayicon.ico new file mode 100644 index 0000000..88fd04f Binary files /dev/null and b/cmd/arcrun-app/build/trayicon.ico differ diff --git a/cmd/arcrun-app/build/trayicon.png b/cmd/arcrun-app/build/trayicon.png new file mode 100644 index 0000000..0c7ad4f Binary files /dev/null and b/cmd/arcrun-app/build/trayicon.png differ diff --git a/cmd/arcrun-app/go.mod b/cmd/arcrun-app/go.mod index 8937b39..07a1050 100644 --- a/cmd/arcrun-app/go.mod +++ b/cmd/arcrun-app/go.mod @@ -2,7 +2,10 @@ module arcrun-app go 1.25.0 -require github.com/wailsapp/wails/v2 v2.13.0 +require ( + github.com/energye/systray v1.0.3 + github.com/wailsapp/wails/v2 v2.13.0 +) require ( git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect @@ -29,10 +32,16 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/net v0.54.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect ) // replace github.com/wailsapp/wails/v2 v2.13.0 => /Users/youlinhsieh/go/pkg/mod + +// supervisor 套件在 collector 主模組裡(重起退避/狀態機/round 解析都在那)—— +// 用 replace 指過去,**不複製一份**(複製=兩份會漂移,今天已被 leo 點破過同類問題)。 +require arcrun-rag/collector v0.0.0 + +replace arcrun-rag/collector => ../.. diff --git a/cmd/arcrun-app/go.sum b/cmd/arcrun-app/go.sum index b664a33..955bc0e 100644 --- a/cmd/arcrun-app/go.sum +++ b/cmd/arcrun-app/go.sum @@ -4,6 +4,8 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/energye/systray v1.0.3 h1:XnyjJCeRU5z00bpNOic2fGTKz/7yHZMZjWiGIVXDS+4= +github.com/energye/systray v1.0.3/go.mod h1:HelKhC3PXwv3ryDxbuQqV+7kAxAYNzE5cfdrerGOZTc= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -61,23 +63,23 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.13.0 h1:S7OgXWpj72V91unF8iDWJKbcS9ZpwCT3R0QVru4v2Mg= github.com/wailsapp/wails/v2 v2.13.0/go.mod h1:nVr/wSIEZ7xxKPkzK65mjpKpaOPQI2k4pvLwGR/i4kc= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/arcrun-app/main.go b/cmd/arcrun-app/main.go index 0fa252d..a7660a7 100644 --- a/cmd/arcrun-app/main.go +++ b/cmd/arcrun-app/main.go @@ -1,39 +1,75 @@ package main 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" + 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() { - // Create an instance of the app structure app := NewApp() - // Create application with options err := wails.Run(&options.App{ - Title: "arcrun-app", - Width: 1024, - Height: 768, - AssetServer: &assetserver.Options{ - Assets: assets, + Title: "Arcrun", + Width: 1000, Height: 700, + MinWidth: 820, MinHeight: 560, + AssetServer: &assetserver.Options{Assets: assets}, + OnStartup: func(ctx wailsCtx) { + app.startup(ctx) + startSupervisor() // 🔴 daemon 本體:不啟動就不會同步 + setupTray(app) }, - BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, - OnStartup: app.startup, - Bind: []interface{}{ - app, + // 🔴 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()) } } + +// setupTray 建立選單列 icon。 +// +// 🔴 leo 2026-08-05:「依照 google drive,托盤裡**只剩下按右鍵會結束**, +// 其他設定都在這個頁面,**點擊托盤的 icon 就立刻展開界面**」 +// +// ⇒ 左鍵=直接開視窗(不彈選單);右鍵=只有一項「結束 Arcrun」。 +// 所有設定都在視窗裡,托盤不再是第二套 UI(fyne 版把設定塞在下拉選單, +// leo:「daemon 設定項越來越多,不可能統統塞在下拉選單」)。 +func setupTray(app *App) { + go systray.Run(func() { + systray.SetTemplateIcon(trayIcon, trayIcon) + systray.SetTooltip("Arcrun — 你的知識庫同步小幫手") + + // 左鍵:立刻展開主視窗 + systray.SetOnClick(func(menu systray.IMenu) { app.ShowWindow() }) + + // 右鍵:只有「結束」一項 + quit := systray.AddMenuItem("結束 Arcrun", "停止同步並關閉") + quit.Click(func() { app.Quit() }) + systray.SetOnRClick(func(menu systray.IMenu) { _ = menu.ShowMenu() }) + }, nil) +} diff --git a/cmd/arcrun-app/supervise.go b/cmd/arcrun-app/supervise.go new file mode 100644 index 0000000..0f7e02e --- /dev/null +++ b/cmd/arcrun-app/supervise.go @@ -0,0 +1,56 @@ +package main + +// supervise.go — 看守 collector 子行程(t194) +// +// 🔴 沒有這一段,這個 App 就只是個「會顯示設定的空殼」—— +// daemon 的本體是 `collector direct`,它才在監看資料夾、萃卡、上傳。 +// (寫 Wails 版時我一度漏了它,等於做出一個裝了也不會同步的東西。) +// +// 邊界:**複用既有的 supervisor 套件**(重起退避、狀態機、round 解析都在裡面, +// 含 t191 的 phase:start/done ⇒ 「同步中…」)。這裡只做「找到執行檔、拉起來」。 +import ( + "os" + "path/filepath" + "runtime" + + "arcrun-rag/collector/supervisor" +) + +var sup *supervisor.Supervisor + +// collectorBinPath 找同綑的 collector 執行檔。 +// 規則與 arcrun-tray 一致:跟 App 執行檔同層(build 時會複製進 .app)。 +func collectorBinPath() string { + name := "arcrun-collector" + if runtime.GOOS == "windows" { + name += ".exe" + } + exe, err := os.Executable() + if err != nil { + return name + } + if p, err := filepath.EvalSymlinks(exe); err == nil { + exe = p + } + return filepath.Join(filepath.Dir(exe), name) +} + +// startSupervisor 在 App 啟動時拉起 collector;沒有 config 就先不啟動 +// (使用者還沒連知識庫,啟動只會一直失敗刷 log)。 +func startSupervisor() { + sup = supervisor.New(collectorBinPath(), configPath()) + if _, err := os.Stat(configPath()); err == nil { + sup.Start() + } +} + +// restartWatch 在設定變更後重起看守,讓新設定立刻生效。 +func restartWatch() { + if sup == nil { + return + } + sup.Stop() + if _, err := os.Stat(configPath()); err == nil { + sup.Start() + } +}