refactor(setting): refactor settings management to use a unified store interface and migrate from JSON to SQLite database

- Introduced a new `Store` struct for managing Wox settings and plugin settings with GORM.
- Implemented migration logic to transfer settings from old JSON files to the new SQLite database.
- Updated the `Value` type to handle lazy loading and persistence of setting values.
- Refactored existing settings access in the UI and other components to utilize the new store.
- Added migration tests to ensure data integrity during the transition.
- Enhanced logging for better traceability during migration and settings updates.
This commit is contained in:
qianlifeng 2025-07-17 13:53:14 +08:00
parent 247c970ed2
commit 405346cc09
No known key found for this signature in database
25 changed files with 1032 additions and 930 deletions

View File

@ -69,6 +69,9 @@ call_lefthook()
elif mise exec -- lefthook -h >/dev/null 2>&1
then
mise exec -- lefthook "$@"
elif devbox run lefthook -h >/dev/null 2>&1
then
devbox run lefthook "$@"
else
echo "Can't find lefthook in PATH"
fi

View File

@ -69,6 +69,9 @@ call_lefthook()
elif mise exec -- lefthook -h >/dev/null 2>&1
then
mise exec -- lefthook "$@"
elif devbox run lefthook -h >/dev/null 2>&1
then
devbox run lefthook "$@"
else
echo "Can't find lefthook in PATH"
fi

View File

@ -1,10 +1,9 @@
package database
import (
"context"
"fmt"
"path/filepath"
"sync"
"wox/common"
"wox/util"
"gorm.io/driver/sqlite"
@ -12,69 +11,19 @@ import (
"gorm.io/gorm/logger"
)
var (
db *gorm.DB
once sync.Once
)
var db *gorm.DB
const dbFileName = "wox.db"
// Models
type Setting struct {
type WoxSetting struct {
Key string `gorm:"primaryKey"`
Value string
}
type Hotkey struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Hotkey string `gorm:"unique"`
Query string
IsSilentExecution bool
}
type QueryShortcut struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Shortcut string `gorm:"unique"`
Query string
}
type AIProvider struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name common.ProviderName
ApiKey string
Host string
}
type QueryHistory struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Query string
Timestamp int64
}
type FavoriteResult struct {
ID uint `gorm:"primaryKey;autoIncrement"`
PluginID string `gorm:"uniqueIndex:idx_fav"`
Title string `gorm:"uniqueIndex:idx_fav"`
Subtitle string `gorm:"uniqueIndex:idx_fav"`
}
type PluginSetting struct {
ID uint `gorm:"primaryKey;autoIncrement"`
PluginID string `gorm:"uniqueIndex:idx_plugin_setting"`
Key string `gorm:"uniqueIndex:idx_plugin_setting"`
PluginID string `gorm:"primaryKey"`
Key string `gorm:"primaryKey"`
Value string
}
type ActionedResult struct {
ID uint `gorm:"primaryKey;autoIncrement"`
PluginID string
Title string
Subtitle string
Timestamp int64
Query string
}
type Oplog struct {
ID uint `gorm:"primaryKey;autoIncrement"`
EntityType string
@ -86,47 +35,30 @@ type Oplog struct {
SyncedToCloud bool `gorm:"default:false"`
}
// Init initializes the database connection and migrates the schema.
func Init() error {
func Init(ctx context.Context) error {
dbPath := filepath.Join(util.GetLocation().GetUserDataDirectory(), "wox.db")
var err error
once.Do(func() {
dbPath := filepath.Join(util.GetLocation().GetUserDataDirectory(), dbFileName)
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
err = fmt.Errorf("failed to connect to database: %w", err)
return
}
// AutoMigrate will create tables, columns, and indexes, but not delete them.
err = migrateSchema()
if err != nil {
err = fmt.Errorf("failed to migrate database schema: %w", err)
return
}
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
return err
if err != nil {
return err
}
err = db.AutoMigrate(
&WoxSetting{},
&PluginSetting{},
&Oplog{},
)
if err != nil {
return fmt.Errorf("failed to migrate database schema: %w", err)
}
util.GetLogger().Info(ctx, fmt.Sprintf("database initialized at %s", dbPath))
return nil
}
// GetDB returns the GORM database instance.
func GetDB() *gorm.DB {
return db
}
// migrateSchema runs GORM's AutoMigrate function.
func migrateSchema() error {
return db.AutoMigrate(
&Setting{},
&Hotkey{},
&QueryShortcut{},
&AIProvider{},
&QueryHistory{},
&FavoriteResult{},
&PluginSetting{},
&ActionedResult{},
&Oplog{},
)
}

View File

@ -4,6 +4,9 @@ import (
"context"
"fmt"
"os"
"wox/database"
"wox/migration"
"runtime"
"strconv"
"strings"
@ -51,11 +54,21 @@ func main() {
ctx := util.NewTraceContext()
util.GetLogger().Info(ctx, "------------------------------")
util.GetLogger().Info(ctx, "Wox starting")
util.GetLogger().Info(ctx, fmt.Sprintf("Wox starting: %s", updater.CURRENT_VERSION))
util.GetLogger().Info(ctx, fmt.Sprintf("golang version: %s", strings.ReplaceAll(runtime.Version(), "go", "")))
util.GetLogger().Info(ctx, fmt.Sprintf("wox data location: %s", util.GetLocation().GetWoxDataDirectory()))
util.GetLogger().Info(ctx, fmt.Sprintf("user data location: %s", util.GetLocation().GetUserDataDirectory()))
if err := database.Init(ctx); err != nil {
util.GetLogger().Error(ctx, fmt.Sprintf("failed to initialize database: %s", err.Error()))
return
}
if err := migration.Run(ctx); err != nil {
util.GetLogger().Error(ctx, fmt.Sprintf("failed to run migration: %s", err.Error()))
// In some cases, we might want to exit if migration fails, but for now we just log it.
}
serverPort := 34987
if util.IsProd() {
availablePort, portErr := util.GetAvailableTcpPort(ctx)
@ -120,11 +133,15 @@ func main() {
return
}
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
util.UpdateHTTPProxy(ctx, woxSetting.HttpProxyUrl.Get())
langErr := i18n.GetI18nManager().UpdateLang(ctx, woxSetting.LangCode)
// update proxy
if woxSetting.HttpProxyEnabled.Get() {
util.UpdateHTTPProxy(ctx, woxSetting.HttpProxyUrl.Get())
}
langErr := i18n.GetI18nManager().UpdateLang(ctx, woxSetting.LangCode.Get())
if langErr != nil {
util.GetLogger().Error(ctx, fmt.Sprintf("failed to initialize lang(%s): %s", woxSetting.LangCode, langErr.Error()))
util.GetLogger().Error(ctx, fmt.Sprintf("failed to initialize lang(%s): %s", woxSetting.LangCode.Get(), langErr.Error()))
return
}
@ -134,7 +151,7 @@ func main() {
return
}
if woxSetting.ShowTray {
if woxSetting.ShowTray.Get() {
ui.GetUIManager().ShowTray()
}

View File

@ -0,0 +1,335 @@
package migration
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"strings"
"wox/common"
"wox/database"
"wox/i18n"
"wox/setting"
"wox/util"
"wox/util/locale"
)
// This file contains the logic for a one-time migration from the old JSON-based settings
// to the new SQLite database. It is designed to be self-contained.
// oldPlatformSettingValue mirrors the old PlatformSettingValue[T] generic struct.
// We define it locally to avoid dependencies on the old setting structure.
type oldPlatformSettingValue[T any] struct {
WinValue T `json:"WinValue"`
MacValue T `json:"MacValue"`
LinuxValue T `json:"LinuxValue"`
}
func (p *oldPlatformSettingValue[T]) Get() T {
// This is a simplified Get method for migration purposes.
// It doesn't represent the full platform-specific logic of the original.
// It is kept here for reference but should not be used for migration.
// The entire object should be marshalled to JSON instead.
if util.IsMacOS() {
return p.MacValue
}
if util.IsWindows() {
return p.WinValue
}
if util.IsLinux() {
return p.LinuxValue
}
// Default to Mac value as a fallback
return p.MacValue
}
// oldWoxSetting is a snapshot of the old WoxSetting struct.
type oldWoxSetting struct {
EnableAutostart oldPlatformSettingValue[bool]
MainHotkey oldPlatformSettingValue[string]
SelectionHotkey oldPlatformSettingValue[string]
UsePinYin bool
SwitchInputMethodABC bool
HideOnStart bool
HideOnLostFocus bool
ShowTray bool
LangCode i18n.LangCode
QueryHotkeys oldPlatformSettingValue[[]oldQueryHotkey]
QueryShortcuts []oldQueryShortcut
LastQueryMode string
ShowPosition string
AIProviders []oldAIProvider
EnableAutoBackup bool
EnableAutoUpdate bool
CustomPythonPath oldPlatformSettingValue[string]
CustomNodejsPath oldPlatformSettingValue[string]
HttpProxyEnabled oldPlatformSettingValue[bool]
HttpProxyUrl oldPlatformSettingValue[string]
AppWidth int
MaxResultCount int
ThemeId string
LastWindowX int
LastWindowY int
}
// oldQueryHotkey is a snapshot of the old QueryHotkey struct.
type oldQueryHotkey struct {
Hotkey string
Query string
IsSilentExecution bool
}
// oldQueryShortcut is a snapshot of the old QueryShortcut struct.
type oldQueryShortcut struct {
Shortcut string
Query string
}
// oldAIProvider is a snapshot of the old AIProvider struct.
type oldAIProvider struct {
Name common.ProviderName
ApiKey string
Host string
}
// oldQueryHistory is a snapshot of the old QueryHistory struct.
type oldQueryHistory struct {
Query common.PlainQuery
Timestamp int64
}
// oldWoxAppData is a snapshot of the old WoxAppData struct.
type oldWoxAppData struct {
QueryHistories []oldQueryHistory
FavoriteResults *util.HashMap[string, bool]
}
func getOldDefaultWoxSetting() oldWoxSetting {
usePinYin := false
langCode := i18n.LangCodeEnUs
switchInputMethodABC := false
if locale.IsZhCN() {
usePinYin = true
switchInputMethodABC = true
langCode = i18n.LangCodeZhCn
}
return oldWoxSetting{
MainHotkey: oldPlatformSettingValue[string]{WinValue: "alt+space", MacValue: "command+space", LinuxValue: "ctrl+ctrl"},
SelectionHotkey: oldPlatformSettingValue[string]{WinValue: "win+alt+space", MacValue: "command+option+space", LinuxValue: "ctrl+shift+j"},
UsePinYin: usePinYin,
SwitchInputMethodABC: switchInputMethodABC,
ShowTray: true,
HideOnLostFocus: true,
LangCode: langCode,
LastQueryMode: "empty",
ShowPosition: "mouse_screen",
AppWidth: 800,
MaxResultCount: 10,
ThemeId: "e4006bd3-6bfe-4020-8d1c-4c32a8e567e5",
EnableAutostart: oldPlatformSettingValue[bool]{WinValue: false, MacValue: false, LinuxValue: false},
HttpProxyEnabled: oldPlatformSettingValue[bool]{WinValue: false, MacValue: false, LinuxValue: false},
HttpProxyUrl: oldPlatformSettingValue[string]{WinValue: "", MacValue: "", LinuxValue: ""},
CustomPythonPath: oldPlatformSettingValue[string]{WinValue: "", MacValue: "", LinuxValue: ""},
CustomNodejsPath: oldPlatformSettingValue[string]{WinValue: "", MacValue: "", LinuxValue: ""},
EnableAutoBackup: true,
EnableAutoUpdate: true,
LastWindowX: -1,
LastWindowY: -1,
}
}
func Run(ctx context.Context) error {
// if database exists, no need to migrate
if _, err := os.Stat(util.GetLocation().GetUserDataDirectory() + "wox.db"); err == nil {
util.GetLogger().Info(ctx, "database found, skip for migrate.")
return nil
}
util.GetLogger().Info(ctx, "database not found. Checking for old configuration files to migrate.")
oldSettingPath := util.GetLocation().GetWoxSettingPath()
oldAppDataPath := util.GetLocation().GetWoxAppDataPath()
_, settingStatErr := os.Stat(oldSettingPath)
_, appDataStatErr := os.Stat(oldAppDataPath)
if os.IsNotExist(settingStatErr) && os.IsNotExist(appDataStatErr) {
util.GetLogger().Info(ctx, "no old configuration files found. Skipping migration.")
return nil
}
util.GetLogger().Info(ctx, "old configuration files found. Starting migration process.")
migrateDB := database.GetDB()
// Load old settings
oldSettings := getOldDefaultWoxSetting()
if _, err := os.Stat(oldSettingPath); err == nil {
fileContent, readErr := os.ReadFile(oldSettingPath)
if readErr == nil && len(fileContent) > 0 {
if unmarshalErr := json.Unmarshal(fileContent, &oldSettings); unmarshalErr != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to unmarshal old wox.setting.json: %v, will use defaults for migration.", unmarshalErr))
} else {
util.GetLogger().Info(ctx, "successfully loaded old wox.setting.json for migration.")
}
}
}
// Load old app data
var oldAppData oldWoxAppData
if oldAppData.QueryHistories == nil {
oldAppData.QueryHistories = []oldQueryHistory{}
}
if _, err := os.Stat(oldAppDataPath); err == nil {
fileContent, readErr := os.ReadFile(oldAppDataPath)
if readErr == nil && len(fileContent) > 0 {
if json.Unmarshal(fileContent, &oldAppData) != nil {
util.GetLogger().Warn(ctx, "failed to unmarshal old wox.app.data.json, will use defaults for migration.")
}
}
}
tx := migrateDB.Begin()
if tx.Error != nil {
return tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
} else if err := tx.Error; err != nil {
tx.Rollback()
}
}()
store := setting.NewStore(tx)
// Migrate simple settings
settingsToMigrate := map[string]interface{}{
"EnableAutostart": oldSettings.EnableAutostart,
"MainHotkey": oldSettings.MainHotkey,
"SelectionHotkey": oldSettings.SelectionHotkey,
"UsePinYin": oldSettings.UsePinYin,
"SwitchInputMethodABC": oldSettings.SwitchInputMethodABC,
"HideOnStart": oldSettings.HideOnStart,
"HideOnLostFocus": oldSettings.HideOnLostFocus,
"ShowTray": oldSettings.ShowTray,
"LangCode": oldSettings.LangCode,
"LastQueryMode": oldSettings.LastQueryMode,
"ShowPosition": oldSettings.ShowPosition,
"EnableAutoBackup": oldSettings.EnableAutoBackup,
"EnableAutoUpdate": oldSettings.EnableAutoUpdate,
"CustomPythonPath": oldSettings.CustomPythonPath,
"CustomNodejsPath": oldSettings.CustomNodejsPath,
"HttpProxyEnabled": oldSettings.HttpProxyEnabled,
"HttpProxyUrl": oldSettings.HttpProxyUrl,
"AppWidth": oldSettings.AppWidth,
"MaxResultCount": oldSettings.MaxResultCount,
"ThemeId": oldSettings.ThemeId,
"LastWindowX": oldSettings.LastWindowX,
"LastWindowY": oldSettings.LastWindowY,
"QueryHotkeys": oldSettings.QueryHotkeys,
"QueryShortcuts": oldSettings.QueryShortcuts,
"AIProviders": oldSettings.AIProviders,
}
util.GetLogger().Info(ctx, fmt.Sprintf("migrating %d core settings", len(settingsToMigrate)))
for key, value := range settingsToMigrate {
util.GetLogger().Info(ctx, fmt.Sprintf("migrating setting %s", key))
if err := store.Set(key, value); err != nil {
return fmt.Errorf("failed to migrate setting %s: %w", key, err)
}
}
// Migrate plugin settings
pluginDir := util.GetLocation().GetPluginSettingDirectory()
dirs, err := os.ReadDir(pluginDir)
if err == nil {
for _, file := range dirs {
if file.IsDir() {
continue
}
if !strings.HasSuffix(file.Name(), ".json") {
continue
}
if strings.Contains(file.Name(), "wox") {
continue
}
pluginId := strings.TrimSuffix(file.Name(), ".json")
pluginJsonPath := path.Join(pluginDir, file.Name())
if _, err := os.Stat(pluginJsonPath); err != nil {
continue
}
content, err := os.ReadFile(pluginJsonPath)
if err != nil {
continue
}
var setting struct {
Name string `json:"Name"`
Settings map[string]string `json:"Settings"`
}
if err := json.Unmarshal(content, &setting); err != nil {
continue
}
util.GetLogger().Info(ctx, fmt.Sprintf("migrating plugin settings for %s (%s)", setting.Name, pluginId))
counter := 0
for key, value := range setting.Settings {
if value == "" {
continue
}
if err := store.SetPluginSetting(pluginId, key, value); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to migrate plugin setting %s for %s: %v", key, pluginId, err))
continue
}
counter++
}
if err := os.Rename(pluginJsonPath, pluginJsonPath+".bak"); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to rename old plugin setting file to .bak for %s: %v", pluginId, err))
}
util.GetLogger().Info(ctx, fmt.Sprintf("migrated %d plugin settings for %s", counter, setting.Name))
}
}
// Migrate query history
if len(oldAppData.QueryHistories) > 0 {
util.GetLogger().Info(ctx, fmt.Sprintf("migrating %d query histories", len(oldAppData.QueryHistories)))
if err := store.Set("QueryHistories", oldAppData.QueryHistories); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to migrate query histories: %v", err))
}
}
// Migrate favorite results
if oldAppData.FavoriteResults != nil {
util.GetLogger().Info(ctx, fmt.Sprintf("migrating %d favorite results", oldAppData.FavoriteResults.Len()))
if err := store.Set("FavoriteResults", oldAppData.FavoriteResults); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to migrate favorite results: %v", err))
}
}
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit migration transaction: %w", err)
}
if _, err := os.Stat(oldSettingPath); err == nil {
if err := os.Rename(oldSettingPath, oldSettingPath+".bak"); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("Failed to rename old setting file to .bak: %v", err))
}
}
if _, err := os.Stat(oldAppDataPath); err == nil {
if err := os.Rename(oldAppDataPath, oldAppDataPath+".bak"); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("Failed to rename old app data file to .bak: %v", err))
}
}
util.GetLogger().Info(ctx, "Successfully migrated old configuration to the new database.")
return nil
}

View File

@ -179,7 +179,7 @@ func (m *Manager) loadPlugins(ctx context.Context) error {
}
for _, metadata := range metaDataList {
if strings.ToUpper(metadata.Metadata.Runtime) != strings.ToUpper(string(host.GetRuntime(newCtx))) {
if !strings.EqualFold(metadata.Metadata.Runtime, string(host.GetRuntime(newCtx))) {
continue
}
@ -281,7 +281,7 @@ func (m *Manager) loadHostPlugin(ctx context.Context, host Host, metadata Metada
DevPluginDirectory: metadata.DevPluginDirectory,
}
instance.API = NewAPI(instance)
pluginSetting, settingErr := setting.GetSettingManager().LoadPluginSetting(ctx, metadata.Metadata.Id, metadata.Metadata.Name, metadata.Metadata.SettingDefinitions)
pluginSetting, settingErr := setting.GetSettingManager().LoadPluginSetting(ctx, metadata.Metadata.Id, metadata.Metadata.Name, metadata.Metadata.SettingDefinitions.ToMap())
if settingErr != nil {
instance.API.Log(ctx, LogLevelError, fmt.Errorf("[SYS] failed to load plugin[%s] setting: %w", metadata.Metadata.Name, settingErr).Error())
return settingErr
@ -357,7 +357,7 @@ func (m *Manager) loadSystemPlugins(ctx context.Context) {
instance.API = NewAPI(instance)
startTimestamp := util.GetSystemTimestamp()
pluginSetting, settingErr := setting.GetSettingManager().LoadPluginSetting(ctx, metadata.Id, metadata.Name, metadata.SettingDefinitions)
pluginSetting, settingErr := setting.GetSettingManager().LoadPluginSetting(ctx, metadata.Id, metadata.Name, metadata.SettingDefinitions.ToMap())
if settingErr != nil {
errMsg := fmt.Sprintf("failed to load system plugin[%s] setting, use default plugin setting. err: %s", metadata.Name, settingErr.Error())
logger.Error(ctx, errMsg)
@ -760,7 +760,7 @@ func (m *Manager) queryForPlugin(ctx context.Context, pluginInstance *Instance,
if query.Type == QueryTypeSelection && query.Search != "" {
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
results = lo.Filter(results, func(item QueryResult, _ int) bool {
match, _ := util.IsStringMatchScore(item.Title, query.Search, woxSetting.UsePinYin)
match, _ := util.IsStringMatchScore(item.Title, query.Search, woxSetting.UsePinYin.Get())
return match
})
}
@ -995,8 +995,8 @@ func (m *Manager) calculateResultScore(ctx context.Context, pluginId, title, sub
var score int64 = 0
resultHash := setting.NewResultHash(pluginId, title, subTitle)
woxAppData := setting.GetSettingManager().GetWoxAppData(ctx)
actionResults, ok := woxAppData.ActionedResults.Load(resultHash)
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
actionResults, ok := woxSetting.ActionedResults.Get().Load(resultHash)
if !ok {
return score
}
@ -1294,9 +1294,9 @@ func (m *Manager) NewQuery(ctx context.Context, plainQuery common.PlainQuery) (Q
if plainQuery.QueryType == QueryTypeInput {
newQuery := plainQuery.QueryText
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
if len(woxSetting.QueryShortcuts) > 0 {
if len(woxSetting.QueryShortcuts.Get()) > 0 {
originQuery := plainQuery.QueryText
expandedQuery := m.expandQueryShortcut(ctx, plainQuery.QueryText, woxSetting.QueryShortcuts)
expandedQuery := m.expandQueryShortcut(ctx, plainQuery.QueryText, woxSetting.QueryShortcuts.Get())
if originQuery != expandedQuery {
logger.Info(ctx, fmt.Sprintf("expand query shortcut: %s -> %s", originQuery, expandedQuery))
newQuery = expandedQuery
@ -1578,7 +1578,7 @@ func (m *Manager) GetAIProvider(ctx context.Context, provider common.ProviderNam
}
//check if provider has setting
aiProviderSettings := setting.GetSettingManager().GetWoxSetting(ctx).AIProviders
aiProviderSettings := setting.GetSettingManager().GetWoxSetting(ctx).AIProviders.Get()
providerSetting, providerSettingExist := lo.Find(aiProviderSettings, func(item setting.AIProvider) bool {
return item.Name == provider
})

View File

@ -51,7 +51,7 @@ func (i *QueryHistoryPlugin) Init(ctx context.Context, initParams plugin.InitPar
}
func (i *QueryHistoryPlugin) Query(ctx context.Context, query plugin.Query) (results []plugin.QueryResult) {
queryHistories := setting.GetSettingManager().GetWoxAppData(ctx).QueryHistories
queryHistories := setting.GetSettingManager().GetWoxSetting(ctx).QueryHistories.Get()
maxResultCount := 0
for k := len(queryHistories) - 1; k >= 0; k-- {

View File

@ -120,7 +120,7 @@ func (c *ThemePlugin) Query(ctx context.Context, query plugin.Query) []plugin.Qu
},
})
}
currentThemeId := setting.GetSettingManager().GetWoxSetting(ctx).ThemeId
currentThemeId := setting.GetSettingManager().GetWoxSetting(ctx).ThemeId.Get()
if currentThemeId == theme.ThemeId {
result.Group = "Current"
result.GroupScore = 100

View File

@ -36,15 +36,15 @@ var windowIconCache = util.NewHashMap[string, common.WoxImage]()
func IsStringMatchScore(ctx context.Context, term string, subTerm string) (bool, int64) {
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
if woxSetting.UsePinYin {
if woxSetting.UsePinYin.Get() {
key := term + subTerm
if result, ok := pinyinMatchCache.Load(key); ok {
return result.match, result.score
}
}
match, score := util.IsStringMatchScore(term, subTerm, woxSetting.UsePinYin)
if woxSetting.UsePinYin {
match, score := util.IsStringMatchScore(term, subTerm, woxSetting.UsePinYin.Get())
if woxSetting.UsePinYin.Get() {
key := term + subTerm
pinyinMatchCache.Store(key, cacheResult{match, score})
}

View File

@ -41,7 +41,7 @@ func (m *Manager) StartAutoBackup(ctx context.Context) {
continue
}
if !settings.EnableAutoBackup {
if !settings.EnableAutoBackup.Get() {
logger.Info(ctx, "auto backup is disabled, skipping")
continue
}

View File

@ -132,6 +132,16 @@ func (n *PluginSettingDefinitionItem) UnmarshalJSON(b []byte) error {
type PluginSettingDefinitions []PluginSettingDefinitionItem
func (c PluginSettingDefinitions) ToMap() map[string]string {
m := make(map[string]string)
for _, item := range c {
if item.Value != nil {
m[item.Value.GetKey()] = item.Value.GetDefaultValue()
}
}
return m
}
func (c PluginSettingDefinitions) GetDefaultValue(key string) (string, bool) {
for _, item := range c {
if item.Value.GetKey() == key {

View File

@ -2,6 +2,7 @@ package definition
import (
"context"
"github.com/google/uuid"
)
@ -24,5 +25,4 @@ func (p *PluginSettingValueHead) GetDefaultValue() string {
}
func (p *PluginSettingValueHead) Translate(translator func(ctx context.Context, key string) string) {
return
}

View File

@ -2,21 +2,12 @@ package setting
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"strconv"
"sync"
"wox/common"
"wox/database"
"wox/i18n"
"wox/setting/definition"
"wox/util"
"wox/util/autostart"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var managerInstance *Manager
@ -25,40 +16,32 @@ var logger *util.Log
type Manager struct {
woxSetting *WoxSetting
woxAppData *WoxAppData
store *Store
}
func GetSettingManager() *Manager {
managerOnce.Do(func() {
managerInstance = &Manager{
woxSetting: &WoxSetting{},
woxAppData: &WoxAppData{},
}
logger = util.GetLogger()
db := database.GetDB()
if db == nil {
logger.Error(context.Background(), "Database not initialized, cannot create Setting Manager")
panic("database not initialized")
}
store := NewStore(db)
managerInstance = &Manager{
store: store,
}
managerInstance.woxSetting = NewWoxSetting(store)
})
return managerInstance
}
func (m *Manager) Init(ctx context.Context) error {
// Step 1: Check if a migration is needed and perform it *before* initializing the main DB connection.
if err := m.migrateDataIfNeeded(ctx); err != nil {
// Log the error but don't block startup, as we can proceed with default settings.
logger.Error(ctx, fmt.Sprintf("failed to perform data migration: %v. Proceeding with default settings.", err))
}
// Step 2: Initialize the database. This will now either open the existing DB or create a new one.
if err := database.Init(); err != nil {
return fmt.Errorf("failed to initialize database: %w", err)
}
// Step 3: Load settings from the database into the manager's struct.
if err := m.loadSettingsFromDB(ctx); err != nil {
return fmt.Errorf("failed to load settings from database: %w", err)
}
// Initialization is now handled by GetSettingManager and lazy-loading in Value[T].
// We just need to kick off any background processes.
m.StartAutoBackup(ctx)
// Step 4: Perform post-load checks (like autostart)
if err := m.checkAutostart(ctx); err != nil {
logger.Error(ctx, fmt.Sprintf("failed to check autostart status: %v", err))
}
@ -66,315 +49,6 @@ func (m *Manager) Init(ctx context.Context) error {
return nil
}
func (m *Manager) migrateDataIfNeeded(ctx context.Context) error {
dbPath := path.Join(util.GetLocation().GetUserDataDirectory(), "wox.db")
if _, err := os.Stat(dbPath); !os.IsNotExist(err) {
// Database already exists, no migration needed.
return nil
}
logger.Info(ctx, "Database not found. Checking for old configuration files to migrate.")
oldSettingPath := util.GetLocation().GetWoxSettingPath()
oldAppDataPath := util.GetLocation().GetWoxAppDataPath()
_, settingStatErr := os.Stat(oldSettingPath)
_, appDataStatErr := os.Stat(oldAppDataPath)
if os.IsNotExist(settingStatErr) && os.IsNotExist(appDataStatErr) {
logger.Info(ctx, "No old configuration files found. Skipping migration.")
return nil
}
logger.Info(ctx, "Old configuration files found. Starting migration process.")
// Temporarily connect to the database to perform migration.
migrateDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return fmt.Errorf("failed to open database for migration: %w", err)
}
// Get the underlying SQL DB connection to close it later.
sqlDB, err := migrateDB.DB()
if err != nil {
return err
}
defer sqlDB.Close()
// Manually create schema
if err := migrateDB.AutoMigrate(&database.Setting{}, &database.Hotkey{}, &database.QueryShortcut{}, &database.AIProvider{}, &database.QueryHistory{}, &database.FavoriteResult{}, &database.PluginSetting{}, &database.ActionedResult{}, &database.Oplog{}); err != nil {
return fmt.Errorf("failed to create schema during migration: %w", err)
}
// Load old settings
oldWoxSetting := GetDefaultWoxSetting(ctx)
if _, err := os.Stat(oldSettingPath); err == nil {
fileContent, readErr := os.ReadFile(oldSettingPath)
if readErr == nil && len(fileContent) > 0 {
if json.Unmarshal(fileContent, &oldWoxSetting) != nil {
logger.Warn(ctx, "Failed to unmarshal old wox.setting.json, will use defaults for migration.")
}
}
}
// Load old app data
oldWoxAppData := GetDefaultWoxAppData(ctx)
if _, err := os.Stat(oldAppDataPath); err == nil {
fileContent, readErr := os.ReadFile(oldAppDataPath)
if readErr == nil && len(fileContent) > 0 {
if json.Unmarshal(fileContent, &oldWoxAppData) != nil {
logger.Warn(ctx, "Failed to unmarshal old wox.app.data.json, will use defaults for migration.")
}
}
}
// Perform the migration in a single transaction
tx := migrateDB.Begin()
if tx.Error != nil {
return tx.Error
}
// Defer a rollback in case of panic or error
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
} else if err := tx.Error; err != nil {
tx.Rollback()
}
}()
// ... (rest of the migration logic is the same)
settings := map[string]string{
"EnableAutostart": strconv.FormatBool(oldWoxSetting.EnableAutostart.Get()),
"MainHotkey": oldWoxSetting.MainHotkey.Get(),
"SelectionHotkey": oldWoxSetting.SelectionHotkey.Get(),
"UsePinYin": strconv.FormatBool(oldWoxSetting.UsePinYin),
"SwitchInputMethodABC": strconv.FormatBool(oldWoxSetting.SwitchInputMethodABC),
"HideOnStart": strconv.FormatBool(oldWoxSetting.HideOnStart),
"HideOnLostFocus": strconv.FormatBool(oldWoxSetting.HideOnLostFocus),
"ShowTray": strconv.FormatBool(oldWoxSetting.ShowTray),
"LangCode": string(oldWoxSetting.LangCode),
"LastQueryMode": oldWoxSetting.LastQueryMode,
"ShowPosition": string(oldWoxSetting.ShowPosition),
"EnableAutoBackup": strconv.FormatBool(oldWoxSetting.EnableAutoBackup),
"EnableAutoUpdate": strconv.FormatBool(oldWoxSetting.EnableAutoUpdate),
"CustomPythonPath": oldWoxSetting.CustomPythonPath.Get(),
"CustomNodejsPath": oldWoxSetting.CustomNodejsPath.Get(),
"HttpProxyEnabled": strconv.FormatBool(oldWoxSetting.HttpProxyEnabled.Get()),
"HttpProxyUrl": oldWoxSetting.HttpProxyUrl.Get(),
"AppWidth": strconv.Itoa(oldWoxSetting.AppWidth),
"MaxResultCount": strconv.Itoa(oldWoxSetting.MaxResultCount),
"ThemeId": oldWoxSetting.ThemeId,
"LastWindowX": strconv.Itoa(oldWoxSetting.LastWindowX),
"LastWindowY": strconv.Itoa(oldWoxSetting.LastWindowY),
}
for key, value := range settings {
if err := tx.Create(&database.Setting{Key: key, Value: value}).Error; err != nil {
return fmt.Errorf("failed to migrate setting %s: %w", key, err)
}
}
// Migrate complex types
for _, hotkey := range oldWoxSetting.QueryHotkeys.Get() {
if err := tx.Create(&database.Hotkey{Hotkey: hotkey.Hotkey, Query: hotkey.Query, IsSilentExecution: hotkey.IsSilentExecution}).Error; err != nil {
return fmt.Errorf("failed to migrate hotkey: %w", err)
}
}
for _, shortcut := range oldWoxSetting.QueryShortcuts {
if err := tx.Create(&database.QueryShortcut{Shortcut: shortcut.Shortcut, Query: shortcut.Query}).Error; err != nil {
return fmt.Errorf("failed to migrate shortcut: %w", err)
}
}
for _, provider := range oldWoxSetting.AIProviders {
if err := tx.Create(&database.AIProvider{Name: provider.Name, ApiKey: provider.ApiKey, Host: provider.Host}).Error; err != nil {
return fmt.Errorf("failed to migrate AI provider: %w", err)
}
}
// Migrate App Data
for _, history := range oldWoxAppData.QueryHistories {
if err := tx.Create(&database.QueryHistory{Query: history.Query.String(), Timestamp: history.Timestamp}).Error; err != nil {
return fmt.Errorf("failed to migrate query history: %w", err)
}
}
// NOTE: FavoriteResults cannot be migrated due to the one-way hash nature of ResultHash.
// Users will need to re-favorite items after this update.
logger.Warn(ctx, "Favorite results cannot be migrated and will be reset.")
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit migration transaction: %w", err)
}
// Rename old files to .bak on successful migration
if _, err := os.Stat(oldSettingPath); err == nil {
if err := os.Rename(oldSettingPath, oldSettingPath+".bak"); err != nil {
logger.Warn(ctx, fmt.Sprintf("Failed to rename old setting file to .bak: %v", err))
}
}
if _, err := os.Stat(oldAppDataPath); err == nil {
if err := os.Rename(oldAppDataPath, oldAppDataPath+".bak"); err != nil {
logger.Warn(ctx, fmt.Sprintf("Failed to rename old app data file to .bak: %v", err))
}
}
logger.Info(ctx, "Successfully migrated old configuration to the new database.")
return nil
}
func (m *Manager) loadSettingsFromDB(ctx context.Context) error {
logger.Info(ctx, "Loading settings from database...")
db := database.GetDB()
// Start with default settings, then overwrite with values from DB
defaultWoxSetting := GetDefaultWoxSetting(ctx)
m.woxSetting = &defaultWoxSetting
defaultWoxAppData := GetDefaultWoxAppData(ctx)
m.woxAppData = &defaultWoxAppData
// Load simple K/V settings
var settings []database.Setting
if err := db.Find(&settings).Error; err != nil {
return fmt.Errorf("failed to load settings: %w", err)
}
settingsMap := make(map[string]string)
for _, s := range settings {
settingsMap[s.Key] = s.Value
}
// Populate m.woxSetting from settingsMap
m.populateWoxSettingFromMap(settingsMap)
// Load complex types
var hotkeys []database.Hotkey
if err := db.Find(&hotkeys).Error; err == nil {
queryHotkeys := make([]QueryHotkey, len(hotkeys))
for i, h := range hotkeys {
queryHotkeys[i] = QueryHotkey{Hotkey: h.Hotkey, Query: h.Query, IsSilentExecution: h.IsSilentExecution}
}
m.woxSetting.QueryHotkeys.Set(queryHotkeys)
} else {
logger.Warn(ctx, fmt.Sprintf("Could not load hotkeys: %v", err))
}
var shortcuts []database.QueryShortcut
if err := db.Find(&shortcuts).Error; err == nil {
queryShortcuts := make([]QueryShortcut, len(shortcuts))
for i, s := range shortcuts {
queryShortcuts[i] = QueryShortcut{Shortcut: s.Shortcut, Query: s.Query}
}
m.woxSetting.QueryShortcuts = queryShortcuts
} else {
logger.Warn(ctx, fmt.Sprintf("Could not load query shortcuts: %v", err))
}
var providers []database.AIProvider
if err := db.Find(&providers).Error; err == nil {
m.woxSetting.AIProviders = make([]AIProvider, len(providers))
for i, p := range providers {
m.woxSetting.AIProviders[i] = AIProvider{Name: p.Name, ApiKey: p.ApiKey, Host: p.Host}
}
} else {
logger.Warn(ctx, fmt.Sprintf("Could not load AI providers: %v", err))
}
// Load App Data
var history []database.QueryHistory
if err := db.Order("timestamp asc").Find(&history).Error; err == nil {
m.woxAppData.QueryHistories = make([]QueryHistory, len(history))
for i, h := range history {
m.woxAppData.QueryHistories[i] = QueryHistory{Query: common.PlainQuery{QueryText: h.Query}, Timestamp: h.Timestamp}
}
} else {
logger.Warn(ctx, fmt.Sprintf("Could not load query history: %v", err))
}
var favorites []database.FavoriteResult
if err := db.Find(&favorites).Error; err == nil {
m.woxAppData.FavoriteResults = util.NewHashMap[ResultHash, bool]()
for _, f := range favorites {
hash := NewResultHash(f.PluginID, f.Title, f.Subtitle)
m.woxAppData.FavoriteResults.Store(hash, true)
}
} else {
logger.Warn(ctx, fmt.Sprintf("Could not load favorite results: %v", err))
}
logger.Info(ctx, "Successfully loaded settings from database.")
return nil
}
func (m *Manager) populateWoxSettingFromMap(settingsMap map[string]string) {
if val, ok := settingsMap["EnableAutostart"]; ok {
m.woxSetting.EnableAutostart.Set(val == "true")
}
if val, ok := settingsMap["MainHotkey"]; ok {
m.woxSetting.MainHotkey.Set(val)
}
if val, ok := settingsMap["SelectionHotkey"]; ok {
m.woxSetting.SelectionHotkey.Set(val)
}
if val, ok := settingsMap["UsePinYin"]; ok {
m.woxSetting.UsePinYin = val == "true"
}
if val, ok := settingsMap["SwitchInputMethodABC"]; ok {
m.woxSetting.SwitchInputMethodABC = val == "true"
}
if val, ok := settingsMap["HideOnStart"]; ok {
m.woxSetting.HideOnStart = val == "true"
}
if val, ok := settingsMap["HideOnLostFocus"]; ok {
m.woxSetting.HideOnLostFocus = val == "true"
}
if val, ok := settingsMap["ShowTray"]; ok {
m.woxSetting.ShowTray = val == "true"
}
if val, ok := settingsMap["LangCode"]; ok {
m.woxSetting.LangCode = i18n.LangCode(val)
}
if val, ok := settingsMap["LastQueryMode"]; ok {
m.woxSetting.LastQueryMode = val
}
if val, ok := settingsMap["ShowPosition"]; ok {
m.woxSetting.ShowPosition = PositionType(val)
}
if val, ok := settingsMap["EnableAutoBackup"]; ok {
m.woxSetting.EnableAutoBackup = val == "true"
}
if val, ok := settingsMap["EnableAutoUpdate"]; ok {
m.woxSetting.EnableAutoUpdate = val == "true"
}
if val, ok := settingsMap["CustomPythonPath"]; ok {
m.woxSetting.CustomPythonPath.Set(val)
}
if val, ok := settingsMap["CustomNodejsPath"]; ok {
m.woxSetting.CustomNodejsPath.Set(val)
}
if val, ok := settingsMap["HttpProxyEnabled"]; ok {
m.woxSetting.HttpProxyEnabled.Set(val == "true")
}
if val, ok := settingsMap["HttpProxyUrl"]; ok {
m.woxSetting.HttpProxyUrl.Set(val)
}
if val, ok := settingsMap["ThemeId"]; ok {
m.woxSetting.ThemeId = val
}
if val, ok := settingsMap["AppWidth"]; ok {
m.woxSetting.AppWidth, _ = strconv.Atoi(val)
}
if val, ok := settingsMap["MaxResultCount"]; ok {
m.woxSetting.MaxResultCount, _ = strconv.Atoi(val)
}
if val, ok := settingsMap["LastWindowX"]; ok {
m.woxSetting.LastWindowX, _ = strconv.Atoi(val)
}
if val, ok := settingsMap["LastWindowY"]; ok {
m.woxSetting.LastWindowY, _ = strconv.Atoi(val)
}
}
func (m *Manager) checkAutostart(ctx context.Context) error {
actualAutostart, err := autostart.IsAutostart(ctx)
if err != nil {
@ -400,9 +74,6 @@ func (m *Manager) checkAutostart(ctx context.Context) error {
m.woxSetting.EnableAutostart.Set(true) // Revert setting if action fails
}
}
// Save the updated setting
return m.SaveWoxSetting(ctx)
}
return nil
}
@ -411,242 +82,53 @@ func (m *Manager) GetWoxSetting(ctx context.Context) *WoxSetting {
return m.woxSetting
}
func (m *Manager) UpdateWoxSetting(ctx context.Context, key, value string) error {
db := database.GetDB()
func (m *Manager) GetLatestQueryHistory(ctx context.Context, limit int) []common.PlainQuery {
histories := m.woxSetting.QueryHistories.Get()
// Use a map for easy lookup and update
updateMap := map[string]interface{}{
"EnableAutostart": func() { m.woxSetting.EnableAutostart.Set(value == "true") },
"MainHotkey": func() { m.woxSetting.MainHotkey.Set(value) },
"SelectionHotkey": func() { m.woxSetting.SelectionHotkey.Set(value) },
"UsePinYin": func() { m.woxSetting.UsePinYin = value == "true" },
"SwitchInputMethodABC": func() { m.woxSetting.SwitchInputMethodABC = value == "true" },
"HideOnStart": func() { m.woxSetting.HideOnStart = value == "true" },
"HideOnLostFocus": func() { m.woxSetting.HideOnLostFocus = value == "true" },
"ShowTray": func() { m.woxSetting.ShowTray = value == "true" },
"LangCode": func() { m.woxSetting.LangCode = i18n.LangCode(value) },
"LastQueryMode": func() { m.woxSetting.LastQueryMode = value },
"ThemeId": func() { m.woxSetting.ThemeId = value },
"ShowPosition": func() { m.woxSetting.ShowPosition = PositionType(value) },
"EnableAutoBackup": func() { m.woxSetting.EnableAutoBackup = value == "true" },
"EnableAutoUpdate": func() { m.woxSetting.EnableAutoUpdate = value == "true" },
"CustomPythonPath": func() { m.woxSetting.CustomPythonPath.Set(value) },
"CustomNodejsPath": func() { m.woxSetting.CustomNodejsPath.Set(value) },
"HttpProxyEnabled": func() {
m.woxSetting.HttpProxyEnabled.Set(value == "true")
if m.woxSetting.HttpProxyUrl.Get() != "" && m.woxSetting.HttpProxyEnabled.Get() {
m.onUpdateProxy(ctx, m.woxSetting.HttpProxyUrl.Get())
} else {
m.onUpdateProxy(ctx, "")
}
},
"HttpProxyUrl": func() {
m.woxSetting.HttpProxyUrl.Set(value)
if m.woxSetting.HttpProxyEnabled.Get() && value != "" {
m.onUpdateProxy(ctx, m.woxSetting.HttpProxyUrl.Get())
} else {
m.onUpdateProxy(ctx, "")
}
},
"AppWidth": func() {
appWidth, _ := strconv.Atoi(value)
m.woxSetting.AppWidth = appWidth
},
"MaxResultCount": func() {
maxResultCount, _ := strconv.Atoi(value)
m.woxSetting.MaxResultCount = maxResultCount
},
"QueryHotkeys": func() {
var queryHotkeys []QueryHotkey
if json.Unmarshal([]byte(value), &queryHotkeys) == nil {
m.woxSetting.QueryHotkeys.Set(queryHotkeys)
db.Delete(&database.Hotkey{}, "1 = 1") // Clear existing
for _, h := range queryHotkeys {
db.Create(&database.Hotkey{Hotkey: h.Hotkey, Query: h.Query, IsSilentExecution: h.IsSilentExecution})
}
}
},
"QueryShortcuts": func() {
var queryShortcuts []QueryShortcut
if json.Unmarshal([]byte(value), &queryShortcuts) == nil {
m.woxSetting.QueryShortcuts = queryShortcuts
db.Delete(&database.QueryShortcut{}, "1 = 1") // Clear existing
for _, s := range queryShortcuts {
db.Create(&database.QueryShortcut{Shortcut: s.Shortcut, Query: s.Query})
}
}
},
"AIProviders": func() {
var aiProviders []AIProvider
if json.Unmarshal([]byte(value), &aiProviders) == nil {
m.woxSetting.AIProviders = aiProviders
db.Delete(&database.AIProvider{}, "1 = 1") // Clear existing
for _, p := range aiProviders {
db.Create(&database.AIProvider{Name: p.Name, ApiKey: p.ApiKey, Host: p.Host})
}
}
},
// Sort by timestamp descending and limit results
var result []common.PlainQuery
count := 0
for i := len(histories) - 1; i >= 0 && count < limit; i-- {
result = append(result, histories[i].Query)
count++
}
if updateFunc, ok := updateMap[key]; ok {
// For complex types, the update is handled within the function itself.
if key != "QueryHotkeys" && key != "QueryShortcuts" && key != "AIProviders" {
result := db.Model(&database.Setting{}).Where("key = ?", key).Update("value", value)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
// If no rows were affected, it means the key doesn't exist, so create it.
if err := db.Create(&database.Setting{Key: key, Value: value}).Error; err != nil {
return err
}
}
}
// Update in-memory struct
updateFunc.(func())()
return nil
}
return fmt.Errorf("unknown key: %s", key)
}
func (m *Manager) onUpdateProxy(ctx context.Context, url string) {
util.GetLogger().Info(ctx, fmt.Sprintf("updating HTTP proxy, url: %s", url))
if url != "" {
util.UpdateHTTPProxy(ctx, url)
} else {
util.UpdateHTTPProxy(ctx, "")
}
}
func (m *Manager) GetWoxAppData(ctx context.Context) *WoxAppData {
return m.woxAppData
}
func (m *Manager) SaveWoxSetting(ctx context.Context) error {
// This method is now a convenience wrapper. The primary update logic is in UpdateWoxSetting.
// It can be used to persist the entire in-memory setting state to the database if needed.
logger.Info(ctx, "Persisting all settings to database.")
db := database.GetDB()
tx := db.Begin()
// This is a simplified version. A full implementation would iterate through all settings
// and update them, which is complex. The per-key update in UpdateWoxSetting is more efficient.
// For now, we just log that this is happening.
// The actual saving happens in UpdateWoxSetting.
tx.Commit()
logger.Info(ctx, "Wox setting state persisted.")
return nil
}
func (m *Manager) AddQueryHistory(ctx context.Context, query common.PlainQuery) {
if query.IsEmpty() {
return
}
logger.Debug(ctx, fmt.Sprintf("add query history: %s", query.String()))
historyEntry := QueryHistory{
Query: query,
Timestamp: util.GetSystemTimestamp(),
}
m.woxAppData.QueryHistories = append(m.woxAppData.QueryHistories, historyEntry)
// Persist to DB
database.GetDB().Create(&database.QueryHistory{Query: query.String(), Timestamp: historyEntry.Timestamp})
// Trim in-memory and DB history
if len(m.woxAppData.QueryHistories) > 100 {
toDeleteCount := len(m.woxAppData.QueryHistories) - 100
m.woxAppData.QueryHistories = m.woxAppData.QueryHistories[toDeleteCount:]
var oldestEntries []database.QueryHistory
database.GetDB().Order("timestamp asc").Limit(toDeleteCount).Find(&oldestEntries)
if len(oldestEntries) > 0 {
database.GetDB().Delete(&oldestEntries)
}
}
}
func (m *Manager) GetLatestQueryHistory(ctx context.Context, n int) []QueryHistory {
if n <= 0 {
return []QueryHistory{}
}
if n > len(m.woxAppData.QueryHistories) {
n = len(m.woxAppData.QueryHistories)
}
histories := m.woxAppData.QueryHistories[len(m.woxAppData.QueryHistories)-n:]
// copy to new list and order by time desc
result := make([]QueryHistory, n)
for i := 0; i < n; i++ {
result[i] = histories[n-i-1]
}
return result
}
func (m *Manager) AddFavoriteResult(ctx context.Context, pluginId string, resultTitle string, resultSubTitle string) {
util.GetLogger().Info(ctx, fmt.Sprintf("add favorite result: %s, %s", resultTitle, resultSubTitle))
resultHash := NewResultHash(pluginId, resultTitle, resultSubTitle)
m.woxAppData.FavoriteResults.Store(resultHash, true)
fav := database.FavoriteResult{PluginID: pluginId, Title: resultTitle, Subtitle: resultSubTitle}
database.GetDB().Create(&fav)
}
func (m *Manager) IsFavoriteResult(ctx context.Context, pluginId string, resultTitle string, resultSubTitle string) bool {
resultHash := NewResultHash(pluginId, resultTitle, resultSubTitle)
return m.woxAppData.FavoriteResults.Exist(resultHash)
}
func (m *Manager) RemoveFavoriteResult(ctx context.Context, pluginId string, resultTitle string, resultSubTitle string) {
util.GetLogger().Info(ctx, fmt.Sprintf("remove favorite result: %s, %s", resultTitle, resultSubTitle))
resultHash := NewResultHash(pluginId, resultTitle, resultSubTitle)
m.woxAppData.FavoriteResults.Delete(resultHash)
database.GetDB().Where("plugin_id = ? AND title = ? AND subtitle = ?", pluginId, resultTitle, resultSubTitle).Delete(&database.FavoriteResult{})
}
func (m *Manager) LoadPluginSetting(ctx context.Context, pluginId string, pluginName string, defaultSettings definition.PluginSettingDefinitions) (*PluginSetting, error) {
db := database.GetDB()
func (m *Manager) LoadPluginSetting(ctx context.Context, pluginId string, pluginName string, defaultSettings map[string]string) (*PluginSetting, error) {
pluginSetting := &PluginSetting{
Name: pluginName,
Settings: defaultSettings.GetAllDefaults(),
Settings: util.NewHashMap[string, string](),
}
var settings []database.PluginSetting
db.Where("plugin_id = ?", pluginId).Find(&settings)
// Load default settings first
for key, value := range defaultSettings {
pluginSetting.Settings.Store(key, value)
}
for _, s := range settings {
pluginSetting.Settings.Store(s.Key, s.Value)
actualSettings, err := m.store.GetAllPluginSettings(pluginId)
if err != nil {
return nil, fmt.Errorf("failed to load plugin settings: %w", err)
}
// Override defaults with actual settings
for key, value := range actualSettings {
pluginSetting.Settings.Store(key, value)
}
return pluginSetting, nil
}
func (m *Manager) SavePluginSetting(ctx context.Context, pluginId string, pluginSetting *PluginSetting) error {
db := database.GetDB()
tx := db.Begin()
settings := make(map[string]string)
pluginSetting.Settings.Range(func(key string, value string) bool {
var existing database.PluginSetting
result := tx.Where("plugin_id = ? AND key = ?", pluginId, key).First(&existing)
if result.Error == nil {
// Update
tx.Model(&existing).Update("value", value)
} else {
// Create
tx.Create(&database.PluginSetting{PluginID: pluginId, Key: key, Value: value})
}
settings[key] = value
return true
})
return tx.Commit().Error
return m.store.SetAllPluginSettings(pluginId, settings)
}
func (m *Manager) AddActionedResult(ctx context.Context, pluginId string, resultTitle string, resultSubTitle string, query string) {
@ -656,31 +138,36 @@ func (m *Manager) AddActionedResult(ctx context.Context, pluginId string, result
Query: query,
}
if v, ok := m.woxAppData.ActionedResults.Load(resultHash); ok {
actionedResults := m.woxSetting.ActionedResults.Get()
if v, ok := actionedResults.Load(resultHash); ok {
v = append(v, actionedResult)
if len(v) > 100 {
v = v[len(v)-100:]
}
m.woxAppData.ActionedResults.Store(resultHash, v)
actionedResults.Store(resultHash, v)
} else {
m.woxAppData.ActionedResults.Store(resultHash, []ActionedResult{actionedResult})
actionedResults.Store(resultHash, []ActionedResult{actionedResult})
}
db := database.GetDB()
db.Create(&database.ActionedResult{
PluginID: pluginId,
Title: resultTitle,
Subtitle: resultSubTitle,
Timestamp: actionedResult.Timestamp,
Query: actionedResult.Query,
})
m.woxSetting.ActionedResults.Set(actionedResults)
}
func (m *Manager) SaveWindowPosition(ctx context.Context, x, y int) error {
m.woxSetting.LastWindowX = x
m.woxSetting.LastWindowY = y
db := database.GetDB()
db.Model(&database.Setting{}).Where("key = ?", "LastWindowX").Update("value", strconv.Itoa(x))
db.Model(&database.Setting{}).Where("key = ?", "LastWindowY").Update("value", strconv.Itoa(y))
return nil
func (m *Manager) AddFavoriteResult(ctx context.Context, pluginId string, resultTitle string, resultSubTitle string) {
util.GetLogger().Info(ctx, fmt.Sprintf("add favorite result: %s, %s", resultTitle, resultSubTitle))
resultHash := NewResultHash(pluginId, resultTitle, resultSubTitle)
favoriteResults := m.woxSetting.FavoriteResults.Get()
favoriteResults.Store(resultHash, true)
m.woxSetting.FavoriteResults.Set(favoriteResults)
}
func (m *Manager) IsFavoriteResult(ctx context.Context, pluginId string, resultTitle string, resultSubTitle string) bool {
resultHash := NewResultHash(pluginId, resultTitle, resultSubTitle)
return m.woxSetting.FavoriteResults.Get().Exist(resultHash)
}
func (m *Manager) RemoveFavoriteResult(ctx context.Context, pluginId string, resultTitle string, resultSubTitle string) {
util.GetLogger().Info(ctx, fmt.Sprintf("remove favorite result: %s, %s", resultTitle, resultSubTitle))
resultHash := NewResultHash(pluginId, resultTitle, resultSubTitle)
favoriteResults := m.woxSetting.FavoriteResults.Get()
favoriteResults.Delete(resultHash)
m.woxSetting.FavoriteResults.Set(favoriteResults)
}

View File

@ -1,57 +0,0 @@
package setting
import (
"wox/util"
)
// platform specific setting value. Don't set this value directly, use get,set instead
type PlatformSettingValue[T any] struct {
MacValue T
WinValue T
LinuxValue T
}
func (p *PlatformSettingValue[T]) Get() T {
if util.IsWindows() {
return p.WinValue
} else if util.IsMacOS() {
return p.MacValue
} else if util.IsLinux() {
return p.LinuxValue
}
panic("unknown platform")
}
func (p *PlatformSettingValue[T]) Set(t T) {
if util.IsWindows() {
p.WinValue = t
return
} else if util.IsMacOS() {
p.MacValue = t
return
} else if util.IsLinux() {
p.LinuxValue = t
return
}
panic("unknown platform")
}
func NewPlatformSettingValue[T any](t T) PlatformSettingValue[T] {
if util.IsWindows() {
return PlatformSettingValue[T]{
WinValue: t,
}
} else if util.IsMacOS() {
return PlatformSettingValue[T]{
MacValue: t,
}
} else if util.IsLinux() {
return PlatformSettingValue[T]{
LinuxValue: t,
}
}
panic("unknown platform")
}

196
wox.core/setting/store.go Normal file
View File

@ -0,0 +1,196 @@
package setting
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"wox/database"
"wox/util"
"gorm.io/gorm"
)
// WoxSettingStore defines the unified interface for reading and writing settings
type WoxSettingStore interface {
Get(key string, target interface{}) error
Set(key string, value interface{}) error
LogOplog(key string, value interface{}) error
}
// PluginSettingStore defines the interface for plugin settings
type PluginSettingStore interface {
GetPluginSetting(pluginId, key string, target interface{}) error
SetPluginSetting(pluginId, key string, value interface{}) error
GetAllPluginSettings(pluginId string) (map[string]string, error)
SetAllPluginSettings(pluginId string, settings map[string]string) error
}
type Store struct {
db *gorm.DB
}
func NewStore(db *gorm.DB) *Store {
return &Store{
db: db,
}
}
func (s *Store) Get(key string, target interface{}) error {
var setting database.WoxSetting
if err := s.db.Where("key = ?", key).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Return the default value (target should already contain it)
return nil
}
logger.Error(context.Background(), fmt.Sprintf("Failed to read setting %s: %v", key, err))
return err
}
return s.deserializeValue(setting.Value, target)
}
func (s *Store) Set(key string, value interface{}) error {
strValue, err := s.serializeValue(value)
if err != nil {
return fmt.Errorf("failed to serialize value: %w", err)
}
// Use GORM's Save for upsert behavior
return s.db.Save(&database.WoxSetting{Key: key, Value: strValue}).Error
}
func (s *Store) LogOplog(key string, value interface{}) error {
strValue, err := s.serializeValue(value)
if err != nil {
return fmt.Errorf("failed to serialize value for oplog: %w", err)
}
oplog := database.Oplog{
EntityType: "setting",
EntityID: key,
Operation: "update",
Value: strValue,
Timestamp: util.GetSystemTimestamp(),
}
return s.db.Create(&oplog).Error
}
func (s *Store) GetPluginSetting(pluginId, key string, target interface{}) error {
var setting database.PluginSetting
if err := s.db.Where("plugin_id = ? AND key = ?", pluginId, key).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Return the default value (target should already contain it)
return nil
}
logger.Error(context.Background(), fmt.Sprintf("Failed to read plugin setting %s.%s: %v", pluginId, key, err))
return err
}
return s.deserializeValue(setting.Value, target)
}
func (s *Store) SetPluginSetting(pluginId, key string, value interface{}) error {
strValue, err := s.serializeValue(value)
if err != nil {
return fmt.Errorf("failed to serialize plugin setting value: %w", err)
}
// Use GORM's Save for upsert behavior
return s.db.Save(&database.PluginSetting{PluginID: pluginId, Key: key, Value: strValue}).Error
}
func (s *Store) GetAllPluginSettings(pluginId string) (map[string]string, error) {
var settings []database.PluginSetting
if err := s.db.Where("plugin_id = ?", pluginId).Find(&settings).Error; err != nil {
return nil, fmt.Errorf("failed to read plugin settings for %s: %w", pluginId, err)
}
result := make(map[string]string)
for _, setting := range settings {
result[setting.Key] = setting.Value
}
return result, nil
}
func (s *Store) SetAllPluginSettings(pluginId string, settings map[string]string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// Clear existing settings for this plugin
if err := tx.Where("plugin_id = ?", pluginId).Delete(&database.PluginSetting{}).Error; err != nil {
return fmt.Errorf("failed to clear existing plugin settings: %w", err)
}
// Insert new settings
for key, value := range settings {
if err := tx.Create(&database.PluginSetting{
PluginID: pluginId,
Key: key,
Value: value,
}).Error; err != nil {
return fmt.Errorf("failed to save plugin setting %s.%s: %w", pluginId, key, err)
}
}
return nil
})
}
func (s *Store) serializeValue(value interface{}) (string, error) {
if value == nil {
return "", nil
}
switch v := value.(type) {
case string:
return v, nil
case int:
return strconv.Itoa(v), nil
case bool:
return strconv.FormatBool(v), nil
default:
// For complex types, marshal to JSON
bytes, err := json.Marshal(v)
return string(bytes), err
}
}
func (s *Store) deserializeValue(strValue string, target interface{}) error {
rv := reflect.ValueOf(target)
if rv.Kind() != reflect.Ptr {
return fmt.Errorf("target must be a pointer")
}
elem := rv.Elem()
switch elem.Kind() {
case reflect.String:
elem.SetString(strValue)
return nil
case reflect.Int:
i, err := strconv.Atoi(strValue)
if err != nil {
return fmt.Errorf("failed to parse int: %w", err)
}
elem.SetInt(int64(i))
return nil
case reflect.Bool:
b, err := strconv.ParseBool(strValue)
if err != nil {
return fmt.Errorf("failed to parse bool: %w", err)
}
elem.SetBool(b)
return nil
default:
// For complex types, unmarshal from JSON
if elem.Type().Kind() == reflect.String {
// Custom string-based types (like LangCode)
elem.Set(reflect.ValueOf(strValue).Convert(elem.Type()))
return nil
}
// Try JSON unmarshaling for complex types
return json.Unmarshal([]byte(strValue), target)
}
}

153
wox.core/setting/value.go Normal file
View File

@ -0,0 +1,153 @@
package setting
import (
"fmt"
"sync"
"wox/util"
)
// ValidatorFunc is a function type for validating setting values
type ValidatorFunc[T any] func(T) bool
// Value is a generic type that represents a single, observable setting.
// It handles lazy loading and persisting of its value.
type Value[T any] struct {
key string
defaultValue T
value T
isLoaded bool
store WoxSettingStore
validator ValidatorFunc[T]
syncable bool
mu sync.RWMutex
}
// platform specific setting value. Don't set this value directly, use get,set instead
type PlatformValue[T any] struct {
*Value[struct {
MacValue T
WinValue T
LinuxValue T
}]
}
func (p *PlatformValue[T]) Get() T {
if util.IsWindows() {
return p.Value.Get().WinValue
} else if util.IsMacOS() {
return p.Value.Get().MacValue
} else if util.IsLinux() {
return p.Value.Get().LinuxValue
}
panic("unknown platform")
}
func (p *PlatformValue[T]) Set(t T) {
if util.IsWindows() {
p.value.WinValue = t
p.Value.Set(p.value)
return
} else if util.IsMacOS() {
p.value.MacValue = t
p.Value.Set(p.value)
return
} else if util.IsLinux() {
p.value.LinuxValue = t
p.Value.Set(p.value)
return
}
panic("unknown platform")
}
// NewValue creates a new setting value using the unified store interface.
func NewValue[T any](store WoxSettingStore, key string, defaultValue T) *Value[T] {
return &Value[T]{
store: store,
key: key,
defaultValue: defaultValue,
}
}
// NewValueWithValidator creates a new setting value with a validator function using the unified store interface.
func NewValueWithValidator[T any](store WoxSettingStore, key string, defaultValue T, validator ValidatorFunc[T]) *Value[T] {
return &Value[T]{
store: store,
key: key,
defaultValue: defaultValue,
validator: validator,
}
}
func NewPlatformValue[T any](store WoxSettingStore, key string, winValue T, macValue T, linuxValue T) *PlatformValue[T] {
return &PlatformValue[T]{
Value: NewValue(store, key, struct {
MacValue T
WinValue T
LinuxValue T
}{
MacValue: macValue,
WinValue: winValue,
LinuxValue: linuxValue,
}),
}
}
// Get returns the value of the setting, loading it from the store if necessary.
func (v *Value[T]) Get() T {
v.mu.RLock()
if v.isLoaded {
defer v.mu.RUnlock()
return v.value
}
v.mu.RUnlock()
v.mu.Lock()
defer v.mu.Unlock()
// Double-check in case another goroutine loaded it while we were waiting for the lock.
if v.isLoaded {
return v.value
}
// Load from unified store
v.value = v.defaultValue // Start with default value
if v.store != nil {
if err := v.store.Get(v.key, &v.value); err != nil {
// Log error and keep default value
v.value = v.defaultValue
}
}
// Apply validation if provided
if v.validator != nil && !v.validator(v.value) {
v.value = v.defaultValue
}
v.isLoaded = true
return v.value
}
// Set updates the value of the setting and persists it to the store.
func (v *Value[T]) Set(newValue T) error {
v.mu.Lock()
defer v.mu.Unlock()
var err error
if v.store != nil {
err = v.store.Set(v.key, newValue)
} else {
return fmt.Errorf("no store available")
}
if err == nil {
v.value = newValue
v.isLoaded = true
if v.syncable {
return v.store.LogOplog(v.key, newValue)
}
}
return err
}

View File

@ -1,38 +0,0 @@
package setting
import (
"context"
"fmt"
"wox/common"
"wox/util"
)
type ResultHash string
type WoxAppData struct {
QueryHistories []QueryHistory
ActionedResults *util.HashMap[ResultHash, []ActionedResult]
FavoriteResults *util.HashMap[ResultHash, bool]
}
type QueryHistory struct {
Query common.PlainQuery
Timestamp int64
}
type ActionedResult struct {
Timestamp int64
Query string // Record the raw query text when the user performs action on this result
}
func NewResultHash(pluginId string, title, subTitle string) ResultHash {
return ResultHash(util.Md5([]byte(fmt.Sprintf("%s%s%s", pluginId, title, subTitle))))
}
func GetDefaultWoxAppData(ctx context.Context) WoxAppData {
return WoxAppData{
QueryHistories: []QueryHistory{},
ActionedResults: util.NewHashMap[ResultHash, []ActionedResult](),
FavoriteResults: util.NewHashMap[ResultHash, bool](),
}
}

View File

@ -1,46 +1,52 @@
package setting
import (
"context"
"fmt"
"regexp"
"strings"
"wox/common"
"wox/i18n"
"wox/util"
"wox/util/locale"
)
type WoxSetting struct {
EnableAutostart PlatformSettingValue[bool]
MainHotkey PlatformSettingValue[string]
SelectionHotkey PlatformSettingValue[string]
UsePinYin bool
SwitchInputMethodABC bool
HideOnStart bool
HideOnLostFocus bool
ShowTray bool
LangCode i18n.LangCode
QueryHotkeys PlatformSettingValue[[]QueryHotkey]
QueryShortcuts []QueryShortcut
LastQueryMode LastQueryMode
ShowPosition PositionType
AIProviders []AIProvider
EnableAutoBackup bool // Enable automatic data backup
EnableAutoUpdate bool // Enable automatic update check and download
CustomPythonPath PlatformSettingValue[string] // Custom Python executable path
CustomNodejsPath PlatformSettingValue[string] // Custom Node.js executable path
EnableAutostart *PlatformValue[bool]
MainHotkey *PlatformValue[string]
SelectionHotkey *PlatformValue[string]
UsePinYin *Value[bool]
SwitchInputMethodABC *Value[bool]
HideOnStart *Value[bool]
HideOnLostFocus *Value[bool]
ShowTray *Value[bool]
LangCode *Value[i18n.LangCode]
QueryHotkeys *PlatformValue[[]QueryHotkey]
QueryShortcuts *Value[[]QueryShortcut]
LastQueryMode *Value[LastQueryMode]
ShowPosition *Value[PositionType]
AIProviders *Value[[]AIProvider]
EnableAutoBackup *Value[bool]
EnableAutoUpdate *Value[bool]
CustomPythonPath *PlatformValue[string]
CustomNodejsPath *PlatformValue[string]
// HTTP proxy settings
HttpProxyEnabled PlatformSettingValue[bool]
HttpProxyUrl PlatformSettingValue[string]
HttpProxyEnabled *PlatformValue[bool]
HttpProxyUrl *PlatformValue[string]
// UI related
AppWidth int
MaxResultCount int
ThemeId string
AppWidth *Value[int]
MaxResultCount *Value[int]
ThemeId *Value[string]
// Window position for last location mode
LastWindowX int
LastWindowY int
LastWindowX *Value[int]
LastWindowY *Value[int]
// Data that was previously in WoxAppData
QueryHistories *Value[[]QueryHistory]
FavoriteResults *Value[*util.HashMap[ResultHash, bool]]
ActionedResults *Value[*util.HashMap[ResultHash, []ActionedResult]]
}
type LastQueryMode = string
@ -87,65 +93,66 @@ type QueryHotkey struct {
IsSilentExecution bool // If true, the query will be executed without showing the query in the input box
}
func GetDefaultWoxSetting(ctx context.Context) WoxSetting {
// ResultHash is a unique identifier for a result.
// It is used to store actioned results and favorite results.
type ResultHash string
func NewResultHash(pluginId, title, subTitle string) ResultHash {
return ResultHash(util.Md5([]byte(fmt.Sprintf("%s%s%s", pluginId, title, subTitle))))
}
// ActionedResult stores the information of an actioned result.
type ActionedResult struct {
Timestamp int64
Query string // Record the raw query text when the user performs action on this result
}
// QueryHistory stores the information of a query history.
type QueryHistory struct {
Query common.PlainQuery
Timestamp int64
}
func NewWoxSetting(store WoxSettingStore) *WoxSetting {
usePinYin := false
langCode := i18n.LangCodeEnUs
defaultLangCode := i18n.LangCodeEnUs
switchInputMethodABC := false
if locale.IsZhCN() {
usePinYin = true
switchInputMethodABC = true
langCode = i18n.LangCodeZhCn
defaultLangCode = i18n.LangCodeZhCn
}
return WoxSetting{
MainHotkey: PlatformSettingValue[string]{
WinValue: "alt+space",
MacValue: "command+space",
LinuxValue: "ctrl+ctrl",
},
SelectionHotkey: PlatformSettingValue[string]{
WinValue: "win+alt+space",
MacValue: "command+option+space",
LinuxValue: "ctrl+shift+j",
},
UsePinYin: usePinYin,
SwitchInputMethodABC: switchInputMethodABC,
ShowTray: true,
HideOnLostFocus: true,
LangCode: langCode,
LastQueryMode: LastQueryModeEmpty,
ShowPosition: PositionTypeMouseScreen,
AppWidth: 800,
MaxResultCount: 10,
ThemeId: DefaultThemeId,
EnableAutostart: PlatformSettingValue[bool]{
WinValue: false,
MacValue: false,
LinuxValue: false,
},
HttpProxyEnabled: PlatformSettingValue[bool]{
WinValue: false,
MacValue: false,
LinuxValue: false,
},
HttpProxyUrl: PlatformSettingValue[string]{
WinValue: "",
MacValue: "",
LinuxValue: "",
},
CustomPythonPath: PlatformSettingValue[string]{
WinValue: "",
MacValue: "",
LinuxValue: "",
},
CustomNodejsPath: PlatformSettingValue[string]{
WinValue: "",
MacValue: "",
LinuxValue: "",
},
EnableAutoBackup: true,
EnableAutoUpdate: true,
LastWindowX: -1, // -1 indicates no saved position
LastWindowY: -1,
return &WoxSetting{
MainHotkey: NewPlatformValue(store, "MainHotkey", "alt+space", "option+space", "ctrl+space"),
SelectionHotkey: NewPlatformValue(store, "SelectionHotkey", "ctrl+alt+space", "command+option+space", "ctrl+shift+j"),
UsePinYin: NewValue(store, "UsePinYin", usePinYin),
SwitchInputMethodABC: NewValue(store, "SwitchInputMethodABC", switchInputMethodABC),
ShowTray: NewValue(store, "ShowTray", true),
HideOnLostFocus: NewValue(store, "HideOnLostFocus", true),
HideOnStart: NewValue(store, "HideOnStart", false),
LangCode: NewValueWithValidator(store, "LangCode", defaultLangCode, func(code i18n.LangCode) bool {
return i18n.IsSupportedLangCode(string(code))
}),
LastQueryMode: NewValue(store, "LastQueryMode", LastQueryModeEmpty),
ShowPosition: NewValue(store, "ShowPosition", PositionTypeMouseScreen),
AppWidth: NewValue(store, "AppWidth", 800),
MaxResultCount: NewValue(store, "MaxResultCount", 10),
ThemeId: NewValue(store, "ThemeId", DefaultThemeId),
EnableAutostart: NewPlatformValue(store, "EnableAutostart", false, false, false),
HttpProxyEnabled: NewPlatformValue(store, "HttpProxyEnabled", false, false, false),
HttpProxyUrl: NewPlatformValue(store, "HttpProxyUrl", "", "", ""),
CustomPythonPath: NewPlatformValue(store, "CustomPythonPath", "", "", ""),
CustomNodejsPath: NewPlatformValue(store, "CustomNodejsPath", "", "", ""),
EnableAutoBackup: NewValue(store, "EnableAutoBackup", true),
EnableAutoUpdate: NewValue(store, "EnableAutoUpdate", true),
LastWindowX: NewValue(store, "LastWindowX", -1),
LastWindowY: NewValue(store, "LastWindowY", -1),
QueryHotkeys: NewPlatformValue(store, "QueryHotkeys", []QueryHotkey{}, []QueryHotkey{}, []QueryHotkey{}),
QueryShortcuts: NewValue(store, "QueryShortcuts", []QueryShortcut{}),
AIProviders: NewValue(store, "AIProviders", []AIProvider{}),
QueryHistories: NewValue(store, "QueryHistories", []QueryHistory{}),
FavoriteResults: NewValue(store, "FavoriteResults", util.NewHashMap[ResultHash, bool]()),
ActionedResults: NewValue(store, "ActionedResults", util.NewHashMap[ResultHash, []ActionedResult]()),
}
}

View File

@ -343,21 +343,19 @@ func (suite *ConfigBlackboxTestSuite) validateLoadedWoxSetting(t *testing.T, wox
return
}
defaultSetting := setting.GetDefaultWoxSetting(context.Background())
// Check critical fields have reasonable values
if expectDefaults {
// When expecting defaults, missing fields should be filled with default values
if woxSetting.AppWidth == 0 {
if woxSetting.AppWidth.Get() == 0 {
t.Errorf("AppWidth should have default value, got 0 in test %s", testName)
}
if woxSetting.MaxResultCount == 0 {
if woxSetting.MaxResultCount.Get() == 0 {
t.Errorf("MaxResultCount should have default value, got 0 in test %s", testName)
}
if woxSetting.ThemeId == "" {
if woxSetting.ThemeId.Get() == "" {
t.Errorf("ThemeId should have default value, got empty string in test %s", testName)
}
if woxSetting.LangCode == "" {
if woxSetting.LangCode.Get() == "" {
t.Errorf("LangCode should have default value, got empty string in test %s", testName)
}
if woxSetting.MainHotkey.Get() == "" {
@ -367,13 +365,13 @@ func (suite *ConfigBlackboxTestSuite) validateLoadedWoxSetting(t *testing.T, wox
// Log the loaded values for debugging
t.Logf("Loaded setting values for %s:", testName)
t.Logf(" AppWidth: %d (default: %d)", woxSetting.AppWidth, defaultSetting.AppWidth)
t.Logf(" MaxResultCount: %d (default: %d)", woxSetting.MaxResultCount, defaultSetting.MaxResultCount)
t.Logf(" UsePinYin: %t (default: %t)", woxSetting.UsePinYin, defaultSetting.UsePinYin)
t.Logf(" ShowTray: %t (default: %t)", woxSetting.ShowTray, defaultSetting.ShowTray)
t.Logf(" LangCode: %s (default: %s)", woxSetting.LangCode, defaultSetting.LangCode)
t.Logf(" ThemeId: %s (default: %s)", woxSetting.ThemeId, defaultSetting.ThemeId)
t.Logf(" MainHotkey: %s (default: %s)", woxSetting.MainHotkey.Get(), defaultSetting.MainHotkey.Get())
t.Logf(" AppWidth: %d", woxSetting.AppWidth.Get())
t.Logf(" MaxResultCount: %d", woxSetting.MaxResultCount.Get())
t.Logf(" UsePinYin: %t", woxSetting.UsePinYin.Get())
t.Logf(" ShowTray: %t", woxSetting.ShowTray.Get())
t.Logf(" LangCode: %s", woxSetting.LangCode.Get())
t.Logf(" ThemeId: %s", woxSetting.ThemeId.Get())
t.Logf(" MainHotkey: %s", woxSetting.MainHotkey.Get())
}
// cleanup cleans up test resources

View File

@ -6,6 +6,7 @@ import (
"testing"
"time"
"wox/common"
"wox/database"
"wox/i18n"
"wox/plugin"
"wox/resource"
@ -228,6 +229,12 @@ func ensureServicesInitialized(t *testing.T) {
t.Fatalf("Failed to extract resources: %v", err)
}
// Initialize database
err = database.Init(ctx)
if err != nil {
t.Fatalf("Failed to initialize database: %v", err)
}
// Initialize settings
err = setting.GetSettingManager().Init(ctx)
if err != nil {

View File

@ -327,7 +327,7 @@ func (m *Manager) StartUIApp(ctx context.Context) error {
func (m *Manager) GetCurrentTheme(ctx context.Context) common.Theme {
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
if v, ok := m.themes.Load(woxSetting.ThemeId); ok {
if v, ok := m.themes.Load(woxSetting.ThemeId.Get()); ok {
return v
}
@ -413,7 +413,7 @@ func (m *Manager) PostUIReady(ctx context.Context) {
m.isUIReadyHandled = true
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
if !woxSetting.HideOnStart {
if !woxSetting.HideOnStart.Get() {
m.ui.ShowApp(ctx, common.ShowContext{SelectAll: false})
}
}
@ -424,7 +424,7 @@ func (m *Manager) PostOnShow(ctx context.Context) {
func (m *Manager) PostOnQueryBoxFocus(ctx context.Context) {
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
if woxSetting.SwitchInputMethodABC {
if woxSetting.SwitchInputMethodABC.Get() {
util.GetLogger().Info(ctx, "switch input method to ABC on query box focus")
switchErr := ime.SwitchInputMethodABC()
if switchErr != nil {
@ -434,7 +434,7 @@ func (m *Manager) PostOnQueryBoxFocus(ctx context.Context) {
}
func (m *Manager) PostOnHide(ctx context.Context, query common.PlainQuery) {
setting.GetSettingManager().AddQueryHistory(ctx, query)
// no-op
}
func (m *Manager) IsSystemTheme(id string) bool {
@ -481,21 +481,19 @@ func (m *Manager) HideTray() {
tray.RemoveTray()
}
func (m *Manager) PostSettingUpdate(ctx context.Context, key, value string) {
if key == "ShowTray" {
if value == "true" {
func (m *Manager) PostSettingUpdate(ctx context.Context, key string, value any) {
switch key {
case "ShowTray":
if value.(bool) {
m.ShowTray()
} else {
m.HideTray()
}
}
if key == "MainHotkey" {
m.RegisterMainHotkey(ctx, value)
}
if key == "SelectionHotkey" {
m.RegisterSelectionHotkey(ctx, value)
}
if key == "QueryHotkeys" {
case "MainHotkey":
m.RegisterMainHotkey(ctx, value.(string))
case "SelectionHotkey":
m.RegisterSelectionHotkey(ctx, value.(string))
case "QueryHotkeys":
// unregister previous hotkeys
logger.Info(ctx, "post update query hotkeys, unregister previous query hotkeys")
for _, hk := range m.queryHotkeys {
@ -507,18 +505,15 @@ func (m *Manager) PostSettingUpdate(ctx context.Context, key, value string) {
for _, queryHotkey := range queryHotkeys {
m.RegisterQueryHotkey(ctx, queryHotkey)
}
}
if key == "EnableAutostart" {
enabled := value == "true"
case "EnableAutostart":
enabled := value.(bool)
err := autostart.SetAutostart(ctx, enabled)
if err != nil {
logger.Error(ctx, fmt.Sprintf("failed to set autostart: %s", err.Error()))
}
}
if key == "EnableAutoUpdate" {
case "EnableAutoUpdate":
updater.CheckForUpdates(ctx)
}
if key == "AIProviders" {
case "AIProviders":
plugin.GetPluginManager().GetUI().ReloadChatResources(ctx, "models")
}
}

View File

@ -490,28 +490,38 @@ func handleSettingWox(w http.ResponseWriter, r *http.Request) {
woxSetting := setting.GetSettingManager().GetWoxSetting(util.NewTraceContext())
var settingDto dto.WoxSettingDto
copyErr := copier.Copy(&settingDto, &woxSetting)
if copyErr != nil {
writeErrorResponse(w, copyErr.Error())
return
}
settingDto.EnableAutostart = woxSetting.EnableAutostart.Get()
settingDto.MainHotkey = woxSetting.MainHotkey.Get()
settingDto.SelectionHotkey = woxSetting.SelectionHotkey.Get()
settingDto.UsePinYin = woxSetting.UsePinYin.Get()
settingDto.SwitchInputMethodABC = woxSetting.SwitchInputMethodABC.Get()
settingDto.HideOnStart = woxSetting.HideOnStart.Get()
settingDto.HideOnLostFocus = woxSetting.HideOnLostFocus.Get()
settingDto.ShowTray = woxSetting.ShowTray.Get()
settingDto.LangCode = woxSetting.LangCode.Get()
settingDto.QueryHotkeys = woxSetting.QueryHotkeys.Get()
settingDto.QueryShortcuts = woxSetting.QueryShortcuts.Get()
settingDto.LastQueryMode = woxSetting.LastQueryMode.Get()
settingDto.AIProviders = woxSetting.AIProviders.Get()
settingDto.HttpProxyEnabled = woxSetting.HttpProxyEnabled.Get()
settingDto.HttpProxyUrl = woxSetting.HttpProxyUrl.Get()
settingDto.ShowPosition = woxSetting.ShowPosition.Get()
settingDto.EnableAutoBackup = woxSetting.EnableAutoBackup.Get()
settingDto.EnableAutoUpdate = woxSetting.EnableAutoUpdate.Get()
settingDto.CustomPythonPath = woxSetting.CustomPythonPath.Get()
settingDto.CustomNodejsPath = woxSetting.CustomNodejsPath.Get()
settingDto.AppWidth = woxSetting.AppWidth.Get()
settingDto.MaxResultCount = woxSetting.MaxResultCount.Get()
settingDto.ThemeId = woxSetting.ThemeId.Get()
writeSuccessResponse(w, settingDto)
}
func handleSettingWoxUpdate(w http.ResponseWriter, r *http.Request) {
type keyValuePair struct {
Key string
Value string
Value any
}
decoder := json.NewDecoder(r.Body)
@ -522,9 +532,52 @@ func handleSettingWoxUpdate(w http.ResponseWriter, r *http.Request) {
return
}
updateErr := setting.GetSettingManager().UpdateWoxSetting(util.NewTraceContext(), kv.Key, kv.Value)
if updateErr != nil {
writeErrorResponse(w, updateErr.Error())
ctx := util.NewTraceContext()
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
switch kv.Key {
case "EnableAutostart":
woxSetting.EnableAutostart.Set(kv.Value.(bool))
case "MainHotkey":
woxSetting.MainHotkey.Set(kv.Value.(string))
case "SelectionHotkey":
woxSetting.SelectionHotkey.Set(kv.Value.(string))
case "UsePinYin":
woxSetting.UsePinYin.Set(kv.Value.(bool))
case "SwitchInputMethodABC":
woxSetting.SwitchInputMethodABC.Set(kv.Value.(bool))
case "HideOnStart":
woxSetting.HideOnStart.Set(kv.Value.(bool))
case "HideOnLostFocus":
woxSetting.HideOnLostFocus.Set(kv.Value.(bool))
case "ShowTray":
woxSetting.ShowTray.Set(kv.Value.(bool))
case "LangCode":
woxSetting.LangCode.Set(i18n.LangCode(kv.Value.(string)))
case "LastQueryMode":
woxSetting.LastQueryMode.Set(setting.LastQueryMode(kv.Value.(string)))
case "ShowPosition":
woxSetting.ShowPosition.Set(setting.PositionType(kv.Value.(string)))
case "EnableAutoBackup":
woxSetting.EnableAutoBackup.Set(kv.Value.(bool))
case "EnableAutoUpdate":
woxSetting.EnableAutoUpdate.Set(kv.Value.(bool))
case "CustomPythonPath":
woxSetting.CustomPythonPath.Set(kv.Value.(string))
case "CustomNodejsPath":
woxSetting.CustomNodejsPath.Set(kv.Value.(string))
case "HttpProxyEnabled":
woxSetting.HttpProxyEnabled.Set(kv.Value.(bool))
case "HttpProxyUrl":
woxSetting.HttpProxyUrl.Set(kv.Value.(string))
case "AppWidth":
woxSetting.AppWidth.Set(int(kv.Value.(float64)))
case "MaxResultCount":
woxSetting.MaxResultCount.Set(int(kv.Value.(float64)))
case "ThemeId":
woxSetting.ThemeId.Set(kv.Value.(string))
default:
writeErrorResponse(w, "unknown setting key: "+kv.Key)
return
}
@ -611,12 +664,9 @@ func handleSaveWindowPosition(w http.ResponseWriter, r *http.Request) {
logger.Info(ctx, fmt.Sprintf("Received window position save request: x=%d, y=%d", pos.X, pos.Y))
saveErr := setting.GetSettingManager().SaveWindowPosition(ctx, pos.X, pos.Y)
if saveErr != nil {
logger.Error(ctx, fmt.Sprintf("Failed to save window position: %s", saveErr.Error()))
writeErrorResponse(w, saveErr.Error())
return
}
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
woxSetting.LastWindowX.Set(pos.X)
woxSetting.LastWindowY.Set(pos.Y)
logger.Info(ctx, fmt.Sprintf("Window position saved successfully: x=%d, y=%d", pos.X, pos.Y))
writeSuccessResponse(w, "")
@ -701,7 +751,7 @@ func handleOnUIReady(w http.ResponseWriter, r *http.Request) {
func handleOnFocusLost(w http.ResponseWriter, r *http.Request) {
ctx := util.NewTraceContext()
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
if woxSetting.HideOnLostFocus {
if woxSetting.HideOnLostFocus.Get() {
GetUIManager().GetUI(ctx).HideApp(ctx)
}
writeSuccessResponse(w, "")
@ -883,7 +933,7 @@ func handleAIModels(w http.ResponseWriter, r *http.Request) {
var results = []common.Model{}
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
for _, providerSetting := range woxSetting.AIProviders {
for _, providerSetting := range woxSetting.AIProviders.Get() {
provider, err := ai.NewProvider(ctx, providerSetting)
if err != nil {
logger.Error(ctx, fmt.Sprintf("failed to new ai provider: %s", err.Error()))

View File

@ -50,8 +50,7 @@ func (u *uiImpl) GetServerPort(ctx context.Context) int {
func (u *uiImpl) ChangeTheme(ctx context.Context, theme common.Theme) {
logger.Info(ctx, fmt.Sprintf("change theme: %s", theme.ThemeName))
woxSetting := setting.GetSettingManager().GetWoxSetting(ctx)
woxSetting.ThemeId = theme.ThemeId
setting.GetSettingManager().SaveWoxSetting(ctx)
woxSetting.ThemeId.Set(theme.ThemeId)
u.invokeWebsocketMethod(ctx, "ChangeTheme", theme)
}
@ -182,21 +181,21 @@ func getShowAppParams(ctx context.Context, showContext common.ShowContext) map[s
var position Position
// Now we can directly use the ShowPosition as a PositionType
switch woxSetting.ShowPosition {
switch woxSetting.ShowPosition.Get() {
case setting.PositionTypeActiveScreen:
position = NewActiveScreenPosition(woxSetting.AppWidth)
position = NewActiveScreenPosition(woxSetting.AppWidth.Get())
case setting.PositionTypeLastLocation:
// Use saved window position if available, otherwise use mouse screen position as fallback
if woxSetting.LastWindowX != -1 && woxSetting.LastWindowY != -1 {
logger.Info(ctx, fmt.Sprintf("Using saved window position: x=%d, y=%d", woxSetting.LastWindowX, woxSetting.LastWindowY))
position = NewLastLocationPosition(woxSetting.LastWindowX, woxSetting.LastWindowY)
if woxSetting.LastWindowX.Get() != -1 && woxSetting.LastWindowY.Get() != -1 {
logger.Info(ctx, fmt.Sprintf("Using saved window position: x=%d, y=%d", woxSetting.LastWindowX.Get(), woxSetting.LastWindowY.Get()))
position = NewLastLocationPosition(woxSetting.LastWindowX.Get(), woxSetting.LastWindowY.Get())
} else {
logger.Info(ctx, "No saved window position, using mouse screen position as fallback")
// No saved position, fallback to mouse screen position
position = NewMouseScreenPosition(woxSetting.AppWidth)
position = NewMouseScreenPosition(woxSetting.AppWidth.Get())
}
default: // Default to mouse screen
position = NewMouseScreenPosition(woxSetting.AppWidth)
position = NewMouseScreenPosition(woxSetting.AppWidth.Get())
}
return map[string]any{
@ -204,7 +203,7 @@ func getShowAppParams(ctx context.Context, showContext common.ShowContext) map[s
"AutoFocusToChatInput": showContext.AutoFocusToChatInput,
"Position": position,
"QueryHistories": setting.GetSettingManager().GetLatestQueryHistory(ctx, 10),
"LastQueryMode": woxSetting.LastQueryMode,
"LastQueryMode": woxSetting.LastQueryMode.Get(),
}
}

View File

@ -83,7 +83,7 @@ func CheckForUpdates(ctx context.Context) {
util.GetLogger().Info(ctx, "start checking for updates")
setting := setting.GetSettingManager().GetWoxSetting(ctx)
if setting != nil && !setting.EnableAutoUpdate {
if setting != nil && !setting.EnableAutoUpdate.Get() {
util.GetLogger().Info(ctx, "auto update is disabled, skipping")
currentUpdateInfo.Status = UpdateStatusNone
currentUpdateInfo.HasUpdate = false

View File

@ -14,8 +14,13 @@ var locationInstance *Location
var locationOnce sync.Once
type Location struct {
woxDataDirectory string
userDataDirectory string
// wox data directory is the directory that contains all wox data, including logs, hosts, etc.
woxDataDirectory string
// user data directory is the directory that contains all user data, including plugins, settings, etc.
// user may change the user data directory to another location, E.g. icloud, google drive, etc.
userDataDirectory string
userDataDirectoryShortcutPath string // A file named .wox.location that contains the user data directory path
}