package main // preferences.go — M8-4:Wails 控制台偏好設定持久化 // // TDD ground truth: // - .autoflow/04-architecture/v2/server-lifecycle.md §11「Preferences 持久化」 // - .autoflow/04-architecture/v2/control-panel.md §4.3(Preferences struct) // // 檔案位置:/preferences.json // 寫入策略:atomic write-rename(tmp → rename),避免 crash 中途造成檔案損毀 // 讀取失敗策略:fallback 到 DefaultPreferences(),不阻止 app 啟動 import ( "encoding/json" "fmt" "os" "path/filepath" "runtime" ) // Preferences 定義控制台偏好。 // 對應 TDD v2/control-panel.md §4.3 的 Preferences struct。 type Preferences struct { // AutoOpenBrowser — StartServer 成功後是否自動開瀏覽器。 // 預設值由 DefaultPreferences() 依 runtime.GOOS 決定: // macOS / Windows → true // Linux → false (R5-D2:Linux 桌面環境差異大,預設關) AutoOpenBrowser bool `json:"autoOpenBrowser"` // Locale — 控制台 UI 的語系覆寫;空字串 → 自動偵測(navigator.language) Locale string `json:"locale,omitempty"` // LogRingSize — log panel ring buffer 行數上限。0 → 使用預設 2000。 LogRingSize int `json:"logRingSize,omitempty"` } // DefaultPreferences 回傳平台相關的預設值。 // // R5-D2:Linux 預設關 AutoOpenBrowser;macOS/Windows 預設開。 func DefaultPreferences() Preferences { return Preferences{ AutoOpenBrowser: runtime.GOOS != "linux", Locale: "", LogRingSize: 0, } } // preferencesPath 回傳 preferences.json 的絕對路徑。 func preferencesPath(dataDir string) string { return filepath.Join(dataDir, "preferences.json") } // LoadPreferences 讀取 preferences.json。 // // 讀取失敗 / 檔案不存在 / JSON 損毀 → 回傳 DefaultPreferences(),不回 error。 // (讀取失敗不應阻止 app 啟動;壞檔留在磁碟供使用者 debug。) func LoadPreferences(dataDir string) Preferences { path := preferencesPath(dataDir) data, err := os.ReadFile(path) if err != nil { // 檔案不存在 / 權限錯誤:用預設 return DefaultPreferences() } var p Preferences if err := json.Unmarshal(data, &p); err != nil { // JSON 壞了:記一行 warning 到 stderr(讓使用者在 log 中看到原因),用預設 fmt.Fprintf(os.Stderr, "[visiona-local] preferences.json parse failed, using defaults: %v\n", err) return DefaultPreferences() } return p } // SavePreferences 把 p 寫入 preferences.json,採 atomic write-rename: // 1. 寫到 .tmp // 2. os.Rename(tmp, real) // // Rename 在同一 filesystem 上是 atomic 的,避免寫到一半 crash 造成檔案損毀。 func SavePreferences(dataDir string, p Preferences) error { if err := os.MkdirAll(dataDir, 0o755); err != nil { return fmt.Errorf("mkdir data dir: %w", err) } data, err := json.MarshalIndent(p, "", " ") if err != nil { return fmt.Errorf("marshal preferences: %w", err) } realPath := preferencesPath(dataDir) tmpPath := realPath + ".tmp" if err := os.WriteFile(tmpPath, data, 0o644); err != nil { return fmt.Errorf("write tmp: %w", err) } if err := os.Rename(tmpPath, realPath); err != nil { _ = os.Remove(tmpPath) // cleanup 失敗的 tmp return fmt.Errorf("rename tmp → real: %w", err) } return nil }