package config import ( "encoding/json" "os" "voicesnap/internal/logger" "voicesnap/internal/model" "voicesnap/internal/paths" ) const ( HotkeyModeTap = "tap" HotkeyModeHold = "hold" LanguageModeAuto = "auto" LanguageModeManual = "manual" ModelSelectionModeAuto = "auto" ModelSelectionModeManual = "manual" ) // Config holds the application configuration, compatible with the WPF version's config.json. type Config struct { HotkeyVK int `json:"HotkeyVK"` HotkeyMode string `json:"HotkeyMode"` AutoHide bool `json:"AutoHide"` SoundFeedback bool `json:"SoundFeedback"` HideDockIcon bool `json:"HideDockIcon"` CopyToClipboard bool `json:"CopyToClipboard"` DeviceName string `json:"DeviceName,omitempty"` IndicatorX int `json:"IndicatorX,omitempty"` IndicatorY int `json:"IndicatorY,omitempty"` SelectedModelID string `json:"SelectedModelID,omitempty"` ModelSelectionMode string `json:"ModelSelectionMode,omitempty"` LanguageMode string `json:"LanguageMode,omitempty"` LanguageID string `json:"LanguageID,omitempty"` ModelDownloadUrl string `json:"ModelDownloadUrl"` FallbackModelDownloadUrl string `json:"FallbackModelDownloadUrl"` } // Default returns a default configuration. func Default() *Config { return &Config{ HotkeyVK: 0xA5, // Right Alt HotkeyMode: HotkeyModeHold, AutoHide: true, SoundFeedback: true, HideDockIcon: true, CopyToClipboard: true, SelectedModelID: model.DefaultModelID, ModelSelectionMode: ModelSelectionModeAuto, LanguageMode: LanguageModeAuto, LanguageID: model.DefaultLanguageID, ModelDownloadUrl: "http://www.maikami.com/voicesnap/sensevoice.zip", FallbackModelDownloadUrl: "https://modelscope.cn/models/sherpa-onnx/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/resolve/master/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17.tar.bz2", } } func configPath() string { return paths.File("config.json") } // Load reads the config from disk. Returns default config on error. func Load() (*Config, error) { path := configPath() data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { cfg := Default() Save(cfg) return cfg, nil } return nil, err } cfg := Default() if err := json.Unmarshal(data, cfg); err != nil { return nil, err } cfg.HotkeyMode = NormalizeHotkeyMode(cfg.HotkeyMode) cfg.LanguageMode = NormalizeLanguageMode(cfg.LanguageMode) cfg.ModelSelectionMode = NormalizeModelSelectionMode(cfg.ModelSelectionMode) languageID := cfg.LanguageID cfg.LanguageID = model.NormalizeLanguageID(cfg.LanguageID) if languageID != "" && languageID != cfg.LanguageID { logger.Info("Unknown LanguageID %q, falling back to %q", languageID, cfg.LanguageID) } selectedModelID := cfg.SelectedModelID cfg.SelectedModelID = model.NormalizeModelID(cfg.SelectedModelID) if selectedModelID != "" && selectedModelID != cfg.SelectedModelID { logger.Info("Unknown SelectedModelID %q, falling back to %q", selectedModelID, cfg.SelectedModelID) } return cfg, nil } func NormalizeLanguageMode(mode string) string { switch mode { case LanguageModeAuto, LanguageModeManual: return mode default: return LanguageModeAuto } } func NormalizeModelSelectionMode(mode string) string { switch mode { case ModelSelectionModeAuto, ModelSelectionModeManual: return mode default: return ModelSelectionModeAuto } } func NormalizeHotkeyMode(mode string) string { switch mode { case HotkeyModeTap, HotkeyModeHold: return mode default: return HotkeyModeHold } } // Save writes the config to disk. func Save(cfg *Config) { if err := paths.Ensure(); err != nil { logger.Error("Failed to create app data dir: %v", err) return } data, err := json.MarshalIndent(cfg, "", " ") if err != nil { logger.Error("Failed to marshal config: %v", err) return } if err := os.WriteFile(configPath(), data, 0644); err != nil { logger.Error("Failed to save config: %v", err) } }