refactor(setting): unify setting store architecture and introduce type-safe value wrappers

- Split Store into separate WoxSettingStore and PluginSettingStore with unified SettingStore interface
- Introduce SettingValue[T] and WoxSettingValue[T] generic wrappers for type-safe setting access
- Refactor PluginSetting to use PluginSettingValue[T] wrappers instead of direct field access
- Remove redundant plugin_setting_test.go and consolidate setting management logic
- Update plugin API, manager, and UI components to work with new setting architecture
- Simplify setting store operations and improve separation of concerns between core and plugin settings

This refactoring provides better type safety, cleaner separation between different setting types,
and a more maintainable architecture for future setting system enhancements.
This commit is contained in:
qianlifeng 2025-07-17 21:06:31 +08:00
parent 405346cc09
commit 734d0f0830
No known key found for this signature in database
14 changed files with 256 additions and 393 deletions

View File

@ -206,7 +206,7 @@ func Run(ctx context.Context) error {
}
}()
store := setting.NewStore(tx)
woxSettingStore := setting.NewWoxSettingStore(tx)
// Migrate simple settings
settingsToMigrate := map[string]interface{}{
@ -241,7 +241,7 @@ func Run(ctx context.Context) error {
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 {
if err := woxSettingStore.Set(key, value); err != nil {
return fmt.Errorf("failed to migrate setting %s: %w", key, err)
}
}
@ -262,6 +262,8 @@ func Run(ctx context.Context) error {
}
pluginId := strings.TrimSuffix(file.Name(), ".json")
pluginSettingStore := setting.NewPluginSettingStore(tx, pluginId)
pluginJsonPath := path.Join(pluginDir, file.Name())
if _, err := os.Stat(pluginJsonPath); err != nil {
continue
@ -285,7 +287,7 @@ func Run(ctx context.Context) error {
if value == "" {
continue
}
if err := store.SetPluginSetting(pluginId, key, value); err != nil {
if err := pluginSettingStore.Set(key, value); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to migrate plugin setting %s for %s: %v", key, pluginId, err))
continue
}
@ -302,7 +304,7 @@ func Run(ctx context.Context) error {
// 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 {
if err := woxSettingStore.Set("QueryHistories", oldAppData.QueryHistories); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to migrate query histories: %v", err))
}
}
@ -310,7 +312,7 @@ func Run(ctx context.Context) error {
// 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 {
if err := woxSettingStore.Set("FavoriteResults", oldAppData.FavoriteResults); err != nil {
util.GetLogger().Warn(ctx, fmt.Sprintf("failed to migrate favorite results: %v", err))
}
}

View File

@ -107,12 +107,12 @@ func (a *APIImpl) GetTranslation(ctx context.Context, key string) string {
func (a *APIImpl) GetSetting(ctx context.Context, key string) string {
// try to get platform specific setting first
platformSpecificKey := key + "@" + util.GetCurrentPlatform()
v, exist := a.pluginInstance.Setting.GetSetting(platformSpecificKey)
v, exist := a.pluginInstance.Setting.Get(platformSpecificKey)
if exist {
return v
}
v, exist = a.pluginInstance.Setting.GetSetting(key)
v, exist = a.pluginInstance.Setting.Get(key)
if exist {
return v
}
@ -125,17 +125,11 @@ func (a *APIImpl) SaveSetting(ctx context.Context, key string, value string, isP
finalKey = key + "@" + util.GetCurrentPlatform()
} else {
// if not platform specific, remove platform specific setting, otherwise it will be loaded first
a.pluginInstance.Setting.Settings.Delete(key + "@" + util.GetCurrentPlatform())
}
existValue, exist := a.pluginInstance.Setting.Settings.Load(finalKey)
a.pluginInstance.Setting.Settings.Store(finalKey, value)
saveErr := a.pluginInstance.SaveSetting(ctx)
if saveErr != nil {
a.logger.Error(ctx, fmt.Sprintf("failed to save setting: %s", saveErr.Error()))
return
a.pluginInstance.Setting.Delete(key + "@" + util.GetCurrentPlatform())
}
existValue, exist := a.pluginInstance.Setting.Get(finalKey)
a.pluginInstance.Setting.Set(finalKey, value)
if !exist || (existValue != value) {
for _, callback := range a.pluginInstance.SettingChangeCallbacks {
callback(key, value)
@ -165,13 +159,12 @@ func (a *APIImpl) OnUnload(ctx context.Context, callback func()) {
}
func (a *APIImpl) RegisterQueryCommands(ctx context.Context, commands []MetadataCommand) {
a.pluginInstance.Setting.QueryCommands = lo.Map(commands, func(command MetadataCommand, _ int) setting.PluginQueryCommand {
a.pluginInstance.Setting.QueryCommands.Set(lo.Map(commands, func(command MetadataCommand, _ int) setting.PluginQueryCommand {
return setting.PluginQueryCommand{
Command: command.Command,
Description: command.Description,
}
})
a.pluginInstance.SaveSetting(ctx)
}))
}
func (a *APIImpl) AIChatStream(ctx context.Context, model common.Model, conversations []common.Conversation, options common.ChatOptions, callback common.ChatStreamFunc) error {

View File

@ -1,7 +1,6 @@
package plugin
import (
"context"
"wox/setting"
)
@ -30,16 +29,18 @@ type Instance struct {
// trigger keywords to trigger this plugin. Maybe user defined or pre-defined in plugin.json
func (i *Instance) GetTriggerKeywords() []string {
if i.Setting.TriggerKeywords != nil {
return i.Setting.TriggerKeywords
var userDefinedKeywords = i.Setting.TriggerKeywords.Get()
if len(userDefinedKeywords) > 0 {
return userDefinedKeywords
}
return i.Metadata.TriggerKeywords
}
// query commands to query this plugin. Maybe plugin author dynamical registered or pre-defined in plugin.json
func (i *Instance) GetQueryCommands() []MetadataCommand {
commands := i.Metadata.Commands
for _, command := range i.Setting.QueryCommands {
for _, command := range i.Setting.QueryCommands.Get() {
commands = append(commands, MetadataCommand{
Command: command.Command,
Description: command.Description,
@ -51,7 +52,3 @@ func (i *Instance) GetQueryCommands() []MetadataCommand {
func (i *Instance) String() string {
return i.Metadata.Name
}
func (i *Instance) SaveSetting(ctx context.Context) error {
return setting.GetSettingManager().SavePluginSetting(ctx, i.Metadata.Id, i.Setting)
}

View File

@ -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.ToMap())
pluginSetting, settingErr := setting.GetSettingManager().LoadPluginSetting(ctx, metadata.Metadata.Id, 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
@ -290,7 +290,7 @@ func (m *Manager) loadHostPlugin(ctx context.Context, host Host, metadata Metada
m.instances = append(m.instances, instance)
if pluginSetting.Disabled {
if pluginSetting.Disabled.Get() {
logger.Info(ctx, fmt.Errorf("[%s HOST] plugin is disabled by user, skip init: %s", host.GetRuntime(ctx), metadata.Metadata.Name).Error())
instance.API.Log(ctx, LogLevelWarning, fmt.Sprintf("[SYS] plugin is disabled by user, skip init: %s", metadata.Metadata.Name))
return nil
@ -357,15 +357,12 @@ 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.ToMap())
pluginSetting, settingErr := setting.GetSettingManager().LoadPluginSetting(ctx, metadata.Id, 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)
instance.API.Log(ctx, LogLevelError, fmt.Sprintf("[SYS] %s", errMsg))
pluginSetting = &setting.PluginSetting{
Settings: util.NewHashMap[string, string](),
}
logger.Error(ctx, fmt.Sprintf("failed to load system plugin[%s] setting, use default plugin setting. err: %s", metadata.Name, settingErr.Error()))
return
}
instance.Setting = pluginSetting
if util.GetSystemTimestamp()-startTimestamp > 100 {
logger.Warn(ctx, fmt.Sprintf("load system plugin[%s] setting too slow, cost %d ms", metadata.Name, util.GetSystemTimestamp()-startTimestamp))
@ -695,7 +692,7 @@ func (m *Manager) GetPluginInstances() []*Instance {
}
func (m *Manager) canOperateQuery(ctx context.Context, pluginInstance *Instance, query Query) bool {
if pluginInstance.Setting.Disabled {
if pluginInstance.Setting.Disabled.Get() {
return false
}

View File

@ -16,7 +16,6 @@ var logger *util.Log
type Manager struct {
woxSetting *WoxSetting
store *Store
}
func GetSettingManager() *Manager {
@ -28,18 +27,14 @@ func GetSettingManager() *Manager {
panic("database not initialized")
}
store := NewStore(db)
managerInstance = &Manager{
store: store,
}
store := NewWoxSettingStore(db)
managerInstance = &Manager{}
managerInstance.woxSetting = NewWoxSetting(store)
})
return managerInstance
}
func (m *Manager) Init(ctx context.Context) error {
// Initialization is now handled by GetSettingManager and lazy-loading in Value[T].
// We just need to kick off any background processes.
m.StartAutoBackup(ctx)
if err := m.checkAutostart(ctx); err != nil {
@ -96,41 +91,12 @@ func (m *Manager) GetLatestQueryHistory(ctx context.Context, limit int) []common
return result
}
func (m *Manager) LoadPluginSetting(ctx context.Context, pluginId string, pluginName string, defaultSettings map[string]string) (*PluginSetting, error) {
pluginSetting := &PluginSetting{
Name: pluginName,
Settings: util.NewHashMap[string, string](),
}
// Load default settings first
for key, value := range defaultSettings {
pluginSetting.Settings.Store(key, 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)
}
func (m *Manager) LoadPluginSetting(ctx context.Context, pluginId string, defaultSettings map[string]string) (*PluginSetting, error) {
pluginSettingStore := NewPluginSettingStore(database.GetDB(), pluginId)
pluginSetting := NewPluginSetting(pluginSettingStore, defaultSettings)
return pluginSetting, nil
}
func (m *Manager) SavePluginSetting(ctx context.Context, pluginId string, pluginSetting *PluginSetting) error {
settings := make(map[string]string)
pluginSetting.Settings.Range(func(key string, value string) bool {
settings[key] = value
return true
})
return m.store.SetAllPluginSettings(pluginId, settings)
}
func (m *Manager) AddActionedResult(ctx context.Context, pluginId string, resultTitle string, resultSubTitle string, query string) {
resultHash := NewResultHash(pluginId, resultTitle, resultSubTitle)
actionedResult := ActionedResult{

View File

@ -1,37 +1,58 @@
package setting
import (
"wox/util"
)
type PluginQueryCommand struct {
Command string
Description string
}
type PluginSetting struct {
Name string // readonly, for display purpose
// Is this plugin disabled by user
Disabled bool
Disabled *PluginSettingValue[bool]
// User defined keywords, will be used to trigger this plugin. User may not set custom trigger keywords, which will cause this property to be null
//
// So don't use this property directly, use Instance.TriggerKeywords instead
TriggerKeywords []string
TriggerKeywords *PluginSettingValue[[]string]
// plugin author can register query command dynamically
// the final query command will be the combination of plugin's metadata commands defined in plugin.json and customized query command registered here
//
// So don't use this directly, use Instance.GetQueryCommands instead
QueryCommands []PluginQueryCommand
QueryCommands *PluginSettingValue[[]PluginQueryCommand]
Settings *util.HashMap[string, string]
store *PluginSettingStore
defaultSettingsInMetadata map[string]string
}
func (p *PluginSetting) GetSetting(key string) (string, bool) {
if p.Settings == nil {
func NewPluginSetting(store *PluginSettingStore, defaultSettingsInMetadata map[string]string) *PluginSetting {
return &PluginSetting{
store: store,
defaultSettingsInMetadata: defaultSettingsInMetadata,
Disabled: NewPluginSettingValue(store, "Disabled", false),
TriggerKeywords: NewPluginSettingValue(store, "TriggerKeywords", []string{}),
QueryCommands: NewPluginSettingValue(store, "QueryCommands", []PluginQueryCommand{}),
}
}
// Try to get the value of the setting. If the setting is not found, return the default value in metadata if exist, otherwise return empty string
func (p *PluginSetting) Get(key string) (string, bool) {
var val string
err := p.store.Get(key, &val)
if err != nil {
if val, ok := p.defaultSettingsInMetadata[key]; ok {
return val, true
}
return "", false
}
return p.Settings.Load(key)
return val, true
}
func (p *PluginSetting) Set(key string, value string) error {
return p.store.Set(key, value)
}
func (p *PluginSetting) Delete(key string) error {
return p.store.Delete(key)
}

View File

@ -1,112 +0,0 @@
package setting
import (
"encoding/json"
"github.com/stretchr/testify/assert"
"testing"
"wox/setting/definition"
"wox/util"
)
func TestUnMarshalPluginSettingItem(t *testing.T) {
type metadataForTest struct {
SettingDefinitions definition.PluginSettingDefinitions
}
jsonStr := `
{
"SettingDefinitions":[
{
"Type":"head",
"Value":{
"Content":"This is head title"
}
},
{
"Type":"textbox",
"Value":{
"Key":"IndexDirectories",
"DefaultValue":"test;test1",
"Label":"Index Directories: ",
"Suffix":" (separate by ';')"
}
},
{
"Type":"checkbox",
"Value":{
"Key":"OnlyIndexTxt",
"DefaultValue": "true",
"Label":", Only Index Txt"
}
},
{
"Type":"select",
"Value":{
"Key":"IndexPrograms",
"DefaultValue":"true",
"Label":"Index Programs: ",
"Options":[
{"Label":"true", "Value":"true"},
{"Label":"false", "Value":"false"}
]
}
},
{
"Type":"newline",
"Value":{}
},
{
"Type":"label",
"Value":{
"Content":"IndexPrograms"
}
}
]
}
`
var metadata metadataForTest
err := json.Unmarshal([]byte(jsonStr), &metadata)
if err != nil {
t.Log(err.Error())
}
assert.Nil(t, err)
assert.Equal(t, len(metadata.SettingDefinitions), 6)
assert.Equal(t, metadata.SettingDefinitions[0].Type, definition.PluginSettingDefinitionTypeHead)
assert.Equal(t, metadata.SettingDefinitions[1].Type, definition.PluginSettingDefinitionTypeTextBox)
assert.Equal(t, metadata.SettingDefinitions[2].Type, definition.PluginSettingDefinitionTypeCheckBox)
assert.Equal(t, metadata.SettingDefinitions[3].Type, definition.PluginSettingDefinitionTypeSelect)
assert.Equal(t, metadata.SettingDefinitions[4].Type, definition.PluginSettingDefinitionTypeNewLine)
assert.Equal(t, metadata.SettingDefinitions[5].Type, definition.PluginSettingDefinitionTypeLabel)
assert.Equal(t, len(metadata.SettingDefinitions[3].Value.(*definition.PluginSettingValueSelect).Options), 2)
val, exist := metadata.SettingDefinitions.GetDefaultValue("IndexDirectories")
assert.True(t, exist)
assert.Equal(t, val, "test;test1")
_, marshalErr := json.Marshal(metadata)
assert.Nil(t, marshalErr)
}
func TestMarshalPluginSetting(t *testing.T) {
var h = util.NewHashMap[string, string]()
h.Store("test", "test")
h.Store("test1", "test")
ps := PluginSetting{
Disabled: true,
TriggerKeywords: nil,
Settings: h,
}
marshalData, marshalErr := json.Marshal(ps)
assert.Nil(t, marshalErr)
t.Log(string(marshalData))
var ps1 PluginSetting
err := json.Unmarshal(marshalData, &ps1)
assert.Nil(t, err)
assert.Equal(t, ps.Disabled, ps1.Disabled)
assert.Equal(t, ps1.Settings.Len(), 2)
}

View File

@ -3,7 +3,6 @@ package setting
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
@ -13,57 +12,53 @@ import (
"gorm.io/gorm"
)
// WoxSettingStore defines the unified interface for reading and writing settings
type WoxSettingStore interface {
// SettingStore defines the abstract interface for reading and writing settings
// This is the base interface that both WoxSettingStore and PluginSettingStore adapters implement
type SettingStore interface {
Get(key string, target interface{}) error
Set(key string, value interface{}) error
Delete(key string) error
}
type SynableStore interface {
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 {
type WoxSettingStore struct {
db *gorm.DB
}
func NewStore(db *gorm.DB) *Store {
return &Store{
func NewWoxSettingStore(db *gorm.DB) *WoxSettingStore {
return &WoxSettingStore{
db: db,
}
}
func (s *Store) Get(key string, target interface{}) error {
func (s *WoxSettingStore) 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))
logger.Error(context.Background(), fmt.Sprintf("Failed to read wox setting %s: %v", key, err))
return err
}
return s.deserializeValue(setting.Value, target)
return deserializeValue(setting.Value, target)
}
func (s *Store) Set(key string, value interface{}) error {
strValue, err := s.serializeValue(value)
func (s *WoxSettingStore) Set(key string, value interface{}) error {
strValue, err := 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)
func (s *WoxSettingStore) Delete(key string) error {
return s.db.Delete(&database.WoxSetting{Key: key}).Error
}
func (s *WoxSettingStore) LogOplog(key string, value interface{}) error {
strValue, err := serializeValue(value)
if err != nil {
return fmt.Errorf("failed to serialize value for oplog: %w", err)
}
@ -79,66 +74,43 @@ func (s *Store) LogOplog(key string, value interface{}) error {
return s.db.Create(&oplog).Error
}
func (s *Store) GetPluginSetting(pluginId, key string, target interface{}) error {
// PluginSettingStore defines the interface for plugin settings
type PluginSettingStore struct {
db *gorm.DB
pluginId string
}
func NewPluginSettingStore(db *gorm.DB, pluginId string) *PluginSettingStore {
return &PluginSettingStore{
db: db,
pluginId: pluginId,
}
}
func (s *PluginSettingStore) Get(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))
if err := s.db.Where("plugin_id = ? AND key = ?", s.pluginId, key).First(&setting).Error; err != nil {
logger.Error(context.Background(), fmt.Sprintf("Failed to read plugin setting %s.%s: %v", s.pluginId, key, err))
return err
}
return s.deserializeValue(setting.Value, target)
return deserializeValue(setting.Value, target)
}
func (s *Store) SetPluginSetting(pluginId, key string, value interface{}) error {
strValue, err := s.serializeValue(value)
func (s *PluginSettingStore) Set(key string, value interface{}) error {
strValue, err := 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
return s.db.Save(&database.PluginSetting{PluginID: s.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 *PluginSettingStore) Delete(key string) error {
return s.db.Delete(&database.PluginSetting{PluginID: s.pluginId, Key: key}).Error
}
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) {
func serializeValue(value interface{}) (string, error) {
if value == nil {
return "", nil
}
@ -157,7 +129,7 @@ func (s *Store) serializeValue(value interface{}) (string, error) {
}
}
func (s *Store) deserializeValue(strValue string, target interface{}) error {
func deserializeValue(strValue string, target interface{}) error {
rv := reflect.ValueOf(target)
if rv.Kind() != reflect.Ptr {
return fmt.Errorf("target must be a pointer")

View File

@ -9,35 +9,46 @@ import (
// 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.
// SettingValue 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
type SettingValue[T any] struct {
key string
value T
defaultValue T
value T
isLoaded bool
store WoxSettingStore
settingStore SettingStore
syncStore SynableStore
validator ValidatorFunc[T]
syncable bool
isLoaded bool
mu sync.RWMutex
}
type WoxSettingValue[T any] struct {
*SettingValue[T]
}
// platform specific setting value. Don't set this value directly, use get,set instead
type PlatformValue[T any] struct {
*Value[struct {
*WoxSettingValue[struct {
MacValue T
WinValue T
LinuxValue T
}]
}
type PluginSettingValue[T any] struct {
*SettingValue[T]
pluginId string
}
func (p *PlatformValue[T]) Get() T {
if util.IsWindows() {
return p.Value.Get().WinValue
return p.SettingValue.Get().WinValue
} else if util.IsMacOS() {
return p.Value.Get().MacValue
return p.SettingValue.Get().MacValue
} else if util.IsLinux() {
return p.Value.Get().LinuxValue
return p.SettingValue.Get().LinuxValue
}
panic("unknown platform")
@ -46,43 +57,45 @@ func (p *PlatformValue[T]) Get() T {
func (p *PlatformValue[T]) Set(t T) {
if util.IsWindows() {
p.value.WinValue = t
p.Value.Set(p.value)
p.SettingValue.Set(p.value)
return
} else if util.IsMacOS() {
p.value.MacValue = t
p.Value.Set(p.value)
p.SettingValue.Set(p.value)
return
} else if util.IsLinux() {
p.value.LinuxValue = t
p.Value.Set(p.value)
p.SettingValue.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,
func NewWoxSettingValue[T any](store *WoxSettingStore, key string, defaultValue T) *WoxSettingValue[T] {
return &WoxSettingValue[T]{
SettingValue: &SettingValue[T]{
settingStore: 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 NewWoxSettingValueWithValidator[T any](store *WoxSettingStore, key string, defaultValue T, validator ValidatorFunc[T]) *WoxSettingValue[T] {
return &WoxSettingValue[T]{
SettingValue: &SettingValue[T]{
settingStore: store,
key: key,
defaultValue: defaultValue,
validator: validator,
},
}
}
func NewPlatformValue[T any](store WoxSettingStore, key string, winValue T, macValue T, linuxValue T) *PlatformValue[T] {
func NewPlatformValue[T any](store *WoxSettingStore, key string, winValue T, macValue T, linuxValue T) *PlatformValue[T] {
return &PlatformValue[T]{
Value: NewValue(store, key, struct {
WoxSettingValue: NewWoxSettingValue(store, key, struct {
MacValue T
WinValue T
LinuxValue T
@ -94,8 +107,19 @@ func NewPlatformValue[T any](store WoxSettingStore, key string, winValue T, macV
}
}
func NewPluginSettingValue[T any](store *PluginSettingStore, key string, defaultValue T) *PluginSettingValue[T] {
return &PluginSettingValue[T]{
SettingValue: &SettingValue[T]{
settingStore: store,
key: key,
defaultValue: defaultValue,
},
pluginId: store.pluginId,
}
}
// Get returns the value of the setting, loading it from the store if necessary.
func (v *Value[T]) Get() T {
func (v *SettingValue[T]) Get() T {
v.mu.RLock()
if v.isLoaded {
defer v.mu.RUnlock()
@ -112,8 +136,8 @@ func (v *Value[T]) Get() T {
// 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 {
if v.settingStore != nil {
if err := v.settingStore.Get(v.key, &v.value); err != nil {
// Log error and keep default value
v.value = v.defaultValue
}
@ -129,25 +153,27 @@ func (v *Value[T]) Get() T {
}
// Set updates the value of the setting and persists it to the store.
func (v *Value[T]) Set(newValue T) error {
func (v *SettingValue[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)
if v.settingStore != nil {
err = v.settingStore.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)
}
if err != nil {
return err
}
return err
v.value = newValue
v.isLoaded = true
if v.syncable {
return v.syncStore.LogOplog(v.key, newValue)
}
return nil
}

View File

@ -14,19 +14,19 @@ type WoxSetting struct {
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]
UsePinYin *WoxSettingValue[bool]
SwitchInputMethodABC *WoxSettingValue[bool]
HideOnStart *WoxSettingValue[bool]
HideOnLostFocus *WoxSettingValue[bool]
ShowTray *WoxSettingValue[bool]
LangCode *WoxSettingValue[i18n.LangCode]
QueryHotkeys *PlatformValue[[]QueryHotkey]
QueryShortcuts *Value[[]QueryShortcut]
LastQueryMode *Value[LastQueryMode]
ShowPosition *Value[PositionType]
AIProviders *Value[[]AIProvider]
EnableAutoBackup *Value[bool]
EnableAutoUpdate *Value[bool]
QueryShortcuts *WoxSettingValue[[]QueryShortcut]
LastQueryMode *WoxSettingValue[LastQueryMode]
ShowPosition *WoxSettingValue[PositionType]
AIProviders *WoxSettingValue[[]AIProvider]
EnableAutoBackup *WoxSettingValue[bool]
EnableAutoUpdate *WoxSettingValue[bool]
CustomPythonPath *PlatformValue[string]
CustomNodejsPath *PlatformValue[string]
@ -35,18 +35,18 @@ type WoxSetting struct {
HttpProxyUrl *PlatformValue[string]
// UI related
AppWidth *Value[int]
MaxResultCount *Value[int]
ThemeId *Value[string]
AppWidth *WoxSettingValue[int]
MaxResultCount *WoxSettingValue[int]
ThemeId *WoxSettingValue[string]
// Window position for last location mode
LastWindowX *Value[int]
LastWindowY *Value[int]
LastWindowX *WoxSettingValue[int]
LastWindowY *WoxSettingValue[int]
// Data that was previously in WoxAppData
QueryHistories *Value[[]QueryHistory]
FavoriteResults *Value[*util.HashMap[ResultHash, bool]]
ActionedResults *Value[*util.HashMap[ResultHash, []ActionedResult]]
QueryHistories *WoxSettingValue[[]QueryHistory]
FavoriteResults *WoxSettingValue[*util.HashMap[ResultHash, bool]]
ActionedResults *WoxSettingValue[*util.HashMap[ResultHash, []ActionedResult]]
}
type LastQueryMode = string
@ -113,7 +113,7 @@ type QueryHistory struct {
Timestamp int64
}
func NewWoxSetting(store WoxSettingStore) *WoxSetting {
func NewWoxSetting(store *WoxSettingStore) *WoxSetting {
usePinYin := false
defaultLangCode := i18n.LangCodeEnUs
switchInputMethodABC := false
@ -126,33 +126,33 @@ func NewWoxSetting(store WoxSettingStore) *WoxSetting {
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 {
UsePinYin: NewWoxSettingValue(store, "UsePinYin", usePinYin),
SwitchInputMethodABC: NewWoxSettingValue(store, "SwitchInputMethodABC", switchInputMethodABC),
ShowTray: NewWoxSettingValue(store, "ShowTray", true),
HideOnLostFocus: NewWoxSettingValue(store, "HideOnLostFocus", true),
HideOnStart: NewWoxSettingValue(store, "HideOnStart", false),
LangCode: NewWoxSettingValueWithValidator(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),
LastQueryMode: NewWoxSettingValue(store, "LastQueryMode", LastQueryModeEmpty),
ShowPosition: NewWoxSettingValue(store, "ShowPosition", PositionTypeMouseScreen),
AppWidth: NewWoxSettingValue(store, "AppWidth", 800),
MaxResultCount: NewWoxSettingValue(store, "MaxResultCount", 10),
ThemeId: NewWoxSettingValue(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),
EnableAutoBackup: NewWoxSettingValue(store, "EnableAutoBackup", true),
EnableAutoUpdate: NewWoxSettingValue(store, "EnableAutoUpdate", true),
LastWindowX: NewWoxSettingValue(store, "LastWindowX", -1),
LastWindowY: NewWoxSettingValue(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]()),
QueryShortcuts: NewWoxSettingValue(store, "QueryShortcuts", []QueryShortcut{}),
AIProviders: NewWoxSettingValue(store, "AIProviders", []AIProvider{}),
QueryHistories: NewWoxSettingValue(store, "QueryHistories", []QueryHistory{}),
FavoriteResults: NewWoxSettingValue(store, "FavoriteResults", util.NewHashMap[ResultHash, bool]()),
ActionedResults: NewWoxSettingValue(store, "ActionedResults", util.NewHashMap[ResultHash, []ActionedResult]()),
}
}

View File

@ -3,7 +3,6 @@ package dto
import (
"wox/common"
"wox/plugin"
"wox/setting"
"wox/setting/definition"
)
@ -23,10 +22,22 @@ type PluginDto struct {
Commands []plugin.MetadataCommand
SupportedOS []string
SettingDefinitions definition.PluginSettingDefinitions // only available when plugin is installed
Setting setting.PluginSetting // only available when plugin is installed
Setting PluginSettingDto // only available when plugin is installed
Features []plugin.MetadataFeature // only available when plugin is installed
IsSystem bool
IsDev bool
IsInstalled bool
IsDisable bool // only available when plugin is installed
}
type PluginSettingDto struct {
Disabled bool
TriggerKeywords []string
QueryCommands []PluginQueryCommandDto
Settings map[string]string
}
type PluginQueryCommandDto struct {
Command string
Description string
}

View File

@ -7,6 +7,7 @@ import (
"net/http"
"strings"
"wox/plugin"
"wox/setting"
"wox/setting/definition"
"wox/ui/dto"
"wox/util"
@ -240,16 +241,25 @@ func convertPluginDto(ctx context.Context, pluginDto dto.PluginDto, pluginInstan
}
}
var definitionSettings = util.NewHashMap[string, string]()
var nonDynamicSettings = make(map[string]string)
for _, item := range pluginDto.SettingDefinitions {
if item.Value != nil {
settingValue := pluginInstance.API.GetSetting(ctx, item.Value.GetKey())
definitionSettings.Store(item.Value.GetKey(), settingValue)
nonDynamicSettings[item.Value.GetKey()] = settingValue
}
}
pluginDto.Setting = *pluginInstance.Setting
//only return user pre-defined settings
pluginDto.Setting.Settings = definitionSettings
pluginDto.Setting = dto.PluginSettingDto{
Disabled: pluginInstance.Setting.Disabled.Get(),
TriggerKeywords: pluginInstance.Setting.TriggerKeywords.Get(),
QueryCommands: lo.Map(pluginInstance.Setting.QueryCommands.Get(), func(item setting.PluginQueryCommand, _ int) dto.PluginQueryCommandDto {
return dto.PluginQueryCommandDto{
Command: item.Command,
Description: item.Description,
}
}),
//only return user pre-defined settings
Settings: nonDynamicSettings,
}
pluginDto.Features = pluginInstance.Metadata.Features
pluginDto.TriggerKeywords = pluginInstance.GetTriggerKeywords()
}

View File

@ -192,7 +192,9 @@ func convertPluginInstanceToDto(ctx context.Context, pluginInstance *plugin.Inst
installedPlugin.IsSystem = pluginInstance.IsSystemPlugin
installedPlugin.IsDev = pluginInstance.IsDevPlugin
installedPlugin.IsInstalled = true
installedPlugin.IsDisable = pluginInstance.Setting.Disabled
installedPlugin.IsDisable = pluginInstance.Setting.Disabled.Get()
installedPlugin.TriggerKeywords = pluginInstance.GetTriggerKeywords()
installedPlugin.Commands = pluginInstance.GetQueryCommands()
//load screenshot urls from store if exist
storePlugin, foundErr := plugin.GetStoreManager().GetStorePluginManifestById(ctx, pluginInstance.Metadata.Id)
@ -230,10 +232,7 @@ func handlePluginInstall(w http.ResponseWriter, r *http.Request) {
plugins := plugin.GetStoreManager().GetStorePluginManifests(ctx)
findPlugin, exist := lo.Find(plugins, func(item plugin.StorePluginManifest) bool {
if item.Id == pluginId {
return true
}
return false
return item.Id == pluginId
})
if !exist {
writeErrorResponse(w, fmt.Sprintf("Plugin '%s' not found in the store", pluginId))
@ -287,8 +286,6 @@ func handlePluginUninstall(w http.ResponseWriter, r *http.Request) {
}
func handlePluginDisable(w http.ResponseWriter, r *http.Request) {
ctx := util.NewTraceContext()
body, _ := io.ReadAll(r.Body)
idResult := gjson.GetBytes(body, "id")
if !idResult.Exists() {
@ -310,19 +307,11 @@ func handlePluginDisable(w http.ResponseWriter, r *http.Request) {
return
}
findPlugin.Setting.Disabled = true
err := findPlugin.SaveSetting(ctx)
if err != nil {
writeErrorResponse(w, "can't disable plugin: "+err.Error())
return
}
findPlugin.Setting.Disabled.Set(true)
writeSuccessResponse(w, "")
}
func handlePluginEnable(w http.ResponseWriter, r *http.Request) {
ctx := util.NewTraceContext()
body, _ := io.ReadAll(r.Body)
idResult := gjson.GetBytes(body, "id")
if !idResult.Exists() {
@ -341,13 +330,7 @@ func handlePluginEnable(w http.ResponseWriter, r *http.Request) {
return
}
findPlugin.Setting.Disabled = false
err := findPlugin.SaveSetting(ctx)
if err != nil {
writeErrorResponse(w, "can't enable plugin: "+err.Error())
return
}
findPlugin.Setting.Disabled.Set(false)
writeSuccessResponse(w, "")
}
@ -587,8 +570,6 @@ func handleSettingWoxUpdate(w http.ResponseWriter, r *http.Request) {
}
func handleSettingPluginUpdate(w http.ResponseWriter, r *http.Request) {
ctx := util.NewTraceContext()
type keyValuePair struct {
PluginId string
Key string
@ -615,11 +596,9 @@ func handleSettingPluginUpdate(w http.ResponseWriter, r *http.Request) {
}
if kv.Key == "Disabled" {
pluginInstance.Setting.Disabled = kv.Value == "true"
pluginInstance.SaveSetting(ctx)
pluginInstance.Setting.Disabled.Set(kv.Value == "true")
} else if kv.Key == "TriggerKeywords" {
pluginInstance.Setting.TriggerKeywords = strings.Split(kv.Value, ",")
pluginInstance.SaveSetting(ctx)
pluginInstance.Setting.TriggerKeywords.Set(strings.Split(kv.Value, ","))
} else {
var isPlatformSpecific = false
for _, settingDefinition := range pluginInstance.Metadata.SettingDefinitions {

View File

@ -13,6 +13,7 @@ import (
"time"
"wox/i18n"
"wox/setting"
"wox/util"
"github.com/Masterminds/semver/v3"