From eb9b037f8e5d80b11dc09c5c8b87e96bfcad5eef Mon Sep 17 00:00:00 2001 From: landaiqing Date: Sun, 22 Jun 2025 15:08:38 +0800 Subject: [PATCH] :art: Optimize code --- .../voidraft/internal/models/models.ts | 23 + .../internal/services/hotkeyservice.ts | 16 +- .../internal/services/keybindingservice.ts | 106 +--- .../voidraft/internal/services/models.ts | 22 - frontend/src/stores/configStore.ts | 8 +- internal/models/config.go | 18 +- .../services/config_notification_service.go | 433 +++++---------- internal/services/config_service.go | 62 +-- internal/services/dialog_service.go | 4 - internal/services/document_service.go | 251 ++++----- internal/services/hotkey_service.go | 331 +++-------- internal/services/hotkey_service_darwin.go | 103 +--- internal/services/hotkey_service_linux.go | 244 +++----- internal/services/hotkey_service_stub.go | 21 +- internal/services/keybinding_service.go | 525 ++---------------- internal/services/migration_service.go | 330 +++++------ internal/services/path_manager.go | 62 +++ internal/services/service_manager.go | 13 +- internal/services/startup_windows.go | 2 +- internal/services/store_service.go | 258 +++++---- internal/services/tray_service.go | 9 - main.go | 2 - 22 files changed, 937 insertions(+), 1906 deletions(-) create mode 100644 internal/services/path_manager.go diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 465615c..2f26024 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -975,9 +975,32 @@ export enum TabType { * UpdatesConfig 更新设置配置 */ export class UpdatesConfig { + /** + * 当前版本号 + */ + "Version": string; + + /** + * 是否自动更新 + */ + "autoUpdate": boolean; + + /** + * 是否启用测试版 + */ + "betaChannel": boolean; /** Creates a new UpdatesConfig instance. */ constructor($$source: Partial = {}) { + if (!("Version" in $$source)) { + this["Version"] = ""; + } + if (!("autoUpdate" in $$source)) { + this["autoUpdate"] = false; + } + if (!("betaChannel" in $$source)) { + this["betaChannel"] = false; + } Object.assign(this, $$source); } diff --git a/frontend/bindings/voidraft/internal/services/hotkeyservice.ts b/frontend/bindings/voidraft/internal/services/hotkeyservice.ts index 3b53efa..4a2fcbd 100644 --- a/frontend/bindings/voidraft/internal/services/hotkeyservice.ts +++ b/frontend/bindings/voidraft/internal/services/hotkeyservice.ts @@ -2,7 +2,7 @@ // This file is automatically generated. DO NOT EDIT /** - * HotkeyService 全局热键服务 + * HotkeyService Windows全局热键服务 * @module */ @@ -18,7 +18,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic import * as models$0 from "../models/models.js"; /** - * GetCurrentHotkey 获取当前注册的热键 + * GetCurrentHotkey 获取当前热键 */ export function GetCurrentHotkey(): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(2572811187) as any; @@ -38,7 +38,7 @@ export function Initialize(app: application$0.App | null): Promise & { can } /** - * IsRegistered 检查是否已注册热键 + * IsRegistered 检查是否已注册 */ export function IsRegistered(): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(106954156) as any; @@ -54,21 +54,13 @@ export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(157291181) as any; return $resultPromise; } -/** - * ToggleWindow 切换窗口显示/隐藏 - 通过事件通知前端处理 - */ -export function ToggleWindow(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1318185132) as any; - return $resultPromise; -} - /** * UnregisterHotkey 取消注册全局热键 */ diff --git a/frontend/bindings/voidraft/internal/services/keybindingservice.ts b/frontend/bindings/voidraft/internal/services/keybindingservice.ts index c8a8b04..a3fed66 100644 --- a/frontend/bindings/voidraft/internal/services/keybindingservice.ts +++ b/frontend/bindings/voidraft/internal/services/keybindingservice.ts @@ -14,34 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime"; // @ts-ignore: Unused imports import * as models$0 from "../models/models.js"; -/** - * DisableKeyBinding 禁用快捷键 - */ -export function DisableKeyBinding(command: models$0.KeyBindingCommand): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1594003006, command) as any; - return $resultPromise; -} - -/** - * EnableKeyBinding 启用快捷键 - */ -export function EnableKeyBinding(command: models$0.KeyBindingCommand): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1462644129, command) as any; - return $resultPromise; -} - -/** - * ExportKeyBindings 导出快捷键配置 - */ -export function ExportKeyBindings(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(4089030977) as any; - let $typingPromise = $resultPromise.then(($result: any) => { - return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; -} - /** * GetAllKeyBindings 获取所有快捷键配置 */ @@ -55,22 +27,10 @@ export function GetAllKeyBindings(): Promise & { cancel() } /** - * GetKeyBindingByCommand 根据命令获取快捷键 + * GetKeyBindingConfig 获取完整快捷键配置 */ -export function GetKeyBindingByCommand(command: models$0.KeyBindingCommand): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3066982544, command) as any; - let $typingPromise = $resultPromise.then(($result: any) => { - return $$createType2($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; -} - -/** - * GetKeyBindingCategories 获取所有快捷键分类 - */ -export function GetKeyBindingCategories(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3141399810) as any; +export function GetKeyBindingConfig(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3804318356) as any; let $typingPromise = $resultPromise.then(($result: any) => { return $$createType3($result); }) as any; @@ -79,65 +39,15 @@ export function GetKeyBindingCategories(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3804318356) as any; - let $typingPromise = $resultPromise.then(($result: any) => { - return $$createType5($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; -} - -/** - * GetKeyBindingsByCategory 根据分类获取快捷键 - */ -export function GetKeyBindingsByCategory(category: models$0.KeyBindingCategory): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1686146606, category) as any; - let $typingPromise = $resultPromise.then(($result: any) => { - return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; -} - -/** - * ImportKeyBindings 导入快捷键配置 - */ -export function ImportKeyBindings(keyBindings: models$0.KeyBinding[]): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(642201520, keyBindings) as any; - return $resultPromise; -} - -/** - * ResetAllKeyBindings 重置所有快捷键到默认值 - */ -export function ResetAllKeyBindings(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2771372645) as any; - return $resultPromise; -} - -/** - * ResetKeyBinding 重置快捷键到默认值 - */ -export function ResetKeyBinding(command: models$0.KeyBindingCommand): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3466323405, command) as any; - return $resultPromise; -} - -/** - * UpdateKeyBinding 更新快捷键 - */ -export function UpdateKeyBinding(command: models$0.KeyBindingCommand, newKey: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1469368983, command, newKey) as any; +export function Shutdown(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3046465148) as any; return $resultPromise; } // Private type creation functions const $$createType0 = models$0.KeyBinding.createFrom; const $$createType1 = $Create.Array($$createType0); -const $$createType2 = $Create.Nullable($$createType0); -const $$createType3 = $Create.Array($Create.Any); -const $$createType4 = models$0.KeyBindingConfig.createFrom; -const $$createType5 = $Create.Nullable($$createType4); +const $$createType2 = models$0.KeyBindingConfig.createFrom; +const $$createType3 = $Create.Nullable($$createType2); diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index f848bad..f2b18ab 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -76,19 +76,8 @@ export class MemoryStats { * MigrationProgress 迁移进度信息 */ export class MigrationProgress { - /** - * 迁移状态 - */ "status": MigrationStatus; - - /** - * 进度百分比 (0-100) - */ "progress": number; - - /** - * 错误信息 - */ "error"?: string; /** Creates a new MigrationProgress instance. */ @@ -121,18 +110,7 @@ export enum MigrationStatus { */ $zero = "", - /** - * 迁移中 - */ MigrationStatusMigrating = "migrating", - - /** - * 完成 - */ MigrationStatusCompleted = "completed", - - /** - * 失败 - */ MigrationStatusFailed = "failed", }; diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index cd9ea54..16627a0 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -143,7 +143,11 @@ const DEFAULT_CONFIG: AppConfig = { language: LanguageType.LangZhCN, systemTheme: SystemThemeType.SystemThemeAuto }, - updates: {}, + updates: { + Version: "1.0.0", + autoUpdate: true, + betaChannel: false + }, metadata: { lastUpdated: new Date().toString() } @@ -276,7 +280,7 @@ export const useConfigStore = defineStore('config', () => { try { // 调用后端重置配置 await safeCall(() => ConfigService.ResetConfig(), 'config.resetFailed', 'config.resetSuccess'); - + // 立即重新加载后端配置以确保前端状态同步 await safeCall(async () => { const appConfig = await ConfigService.GetConfig(); diff --git a/internal/models/config.go b/internal/models/config.go index 3d6e04b..7585def 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -1,6 +1,8 @@ package models import ( + "os" + "path/filepath" "time" ) @@ -82,7 +84,9 @@ type AppearanceConfig struct { // UpdatesConfig 更新设置配置 type UpdatesConfig struct { - // 预留给未来的更新配置 + Version string `json:"Version"` // 当前版本号 + AutoUpdate bool `json:"autoUpdate"` // 是否自动更新 + BetaChannel bool `json:"betaChannel"` // 是否启用测试版 } // AppConfig 应用配置 - 按照前端设置页面分类组织 @@ -101,10 +105,14 @@ type ConfigMetadata struct { // NewDefaultAppConfig 创建默认应用配置 func NewDefaultAppConfig() *AppConfig { + + currentDir, _ := os.UserConfigDir() + dataDir := filepath.Join(currentDir, ".voidraft", "data") + return &AppConfig{ General: GeneralConfig{ AlwaysOnTop: false, - DataPath: "./data", + DataPath: dataDir, EnableSystemTray: true, StartAtLogin: false, EnableGlobalHotkey: false, @@ -133,7 +141,11 @@ func NewDefaultAppConfig() *AppConfig { Language: LangZhCN, SystemTheme: SystemThemeAuto, // 默认使用深色系统主题 }, - Updates: UpdatesConfig{}, + Updates: UpdatesConfig{ + Version: "1.0.0", + AutoUpdate: true, + BetaChannel: false, + }, Metadata: ConfigMetadata{ LastUpdated: time.Now().Format(time.RFC3339), }, diff --git a/internal/services/config_notification_service.go b/internal/services/config_notification_service.go index 531da00..9b863de 100644 --- a/internal/services/config_notification_service.go +++ b/internal/services/config_notification_service.go @@ -1,10 +1,10 @@ package services import ( + "context" "crypto/sha256" "encoding/json" "fmt" - "reflect" "sync" "time" "voidraft/internal/models" @@ -24,106 +24,76 @@ const ( ) // ConfigChangeCallback 配置变更回调函数类型 -type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig interface{}) error +type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error // ConfigListener 配置监听器 type ConfigListener struct { - Name string // 监听器名称 - ChangeType ConfigChangeType // 监听的配置变更类型 - Callback ConfigChangeCallback // 回调函数(现在包含新旧配置) - DebounceDelay time.Duration // 防抖延迟时间 - GetConfigFunc func(*viper.Viper) interface{} // 获取相关配置的函数 + Name string // 监听器名称 + ChangeType ConfigChangeType // 监听的配置变更类型 + Callback ConfigChangeCallback // 回调函数(现在包含新旧配置) + DebounceDelay time.Duration // 防抖延迟时间 + GetConfigFunc func(*viper.Viper) *models.AppConfig // 获取相关配置的函数 // 内部状态 - mu sync.RWMutex // 监听器状态锁 - timer *time.Timer // 防抖定时器 - lastConfigHash string // 上次配置的哈希值,用于变更检测 - lastConfig interface{} // 上次的配置副本 - stopChan chan struct{} // 停止通道,用于停止异步goroutine + mu sync.RWMutex // 监听器状态锁 + timer *time.Timer // 防抖定时器 + lastConfigHash string // 上次配置的哈希值,用于变更检测 + lastConfig *models.AppConfig // 上次的配置副本 + ctx context.Context + cancel context.CancelFunc } // ConfigNotificationService 配置通知服务 type ConfigNotificationService struct { - listeners map[ConfigChangeType]*ConfigListener // 监听器映射 - mu sync.RWMutex // 读写锁 - logger *log.LoggerService // 日志服务 - viper *viper.Viper // Viper实例 + listeners sync.Map // 使用sync.Map替代普通map+锁 + logger *log.LoggerService // 日志服务 + viper *viper.Viper // Viper实例 + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup } // NewConfigNotificationService 创建配置通知服务 func NewConfigNotificationService(viper *viper.Viper, logger *log.LoggerService) *ConfigNotificationService { + ctx, cancel := context.WithCancel(context.Background()) return &ConfigNotificationService{ - listeners: make(map[ConfigChangeType]*ConfigListener), - logger: logger, - viper: viper, + logger: logger, + viper: viper, + ctx: ctx, + cancel: cancel, } } // RegisterListener 注册配置监听器 func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error { - cns.mu.Lock() - defer cns.mu.Unlock() - - // 检查是否已存在同类型监听器 - if existingListener, exists := cns.listeners[listener.ChangeType]; exists { - cns.logger.Warning("ConfigNotification: Listener already exists, will be replaced", - "existing_name", existingListener.Name, - "new_name", listener.Name, - "type", string(listener.ChangeType)) - - // 清理旧监听器 - cns.cleanupListener(existingListener) + // 清理已存在的监听器 + if existingValue, loaded := cns.listeners.LoadAndDelete(listener.ChangeType); loaded { + if existing, ok := existingValue.(interface{ cancel() }); ok { + existing.cancel() + } } // 初始化新监听器 - listener.stopChan = make(chan struct{}) - - // 初始化监听器的配置状态 + listener.ctx, listener.cancel = context.WithCancel(cns.ctx) if err := cns.initializeListenerState(listener); err != nil { - cns.logger.Error("ConfigNotification: Failed to initialize listener state", - "listener", listener.Name, - "error", err) + listener.cancel() return fmt.Errorf("failed to initialize listener state: %w", err) } - cns.listeners[listener.ChangeType] = listener - cns.logger.Info("ConfigNotification: Registered listener", - "name", listener.Name, - "type", string(listener.ChangeType)) - + cns.listeners.Store(listener.ChangeType, listener) return nil } -// cleanupListener 清理监听器资源 -func (cns *ConfigNotificationService) cleanupListener(listener *ConfigListener) { - listener.mu.Lock() - defer listener.mu.Unlock() - - // 停止防抖定时器 - if listener.timer != nil { - listener.timer.Stop() - listener.timer = nil - } - - // 关闭停止通道,通知goroutine退出 - if listener.stopChan != nil { - close(listener.stopChan) - listener.stopChan = nil - } -} - // initializeListenerState 初始化监听器状态 func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigListener) error { if listener.GetConfigFunc == nil { return fmt.Errorf("GetConfigFunc is required") } - // 获取初始配置 - config := listener.GetConfigFunc(cns.viper) - if config != nil { + if config := listener.GetConfigFunc(cns.viper); config != nil { listener.mu.Lock() - listener.lastConfig = cns.deepCopy(config) - listener.lastConfigHash = cns.computeConfigHash(config) + listener.lastConfig = deepCopyConfig(config) + listener.lastConfigHash = computeConfigHash(config) listener.mu.Unlock() } @@ -132,205 +102,92 @@ func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigLi // UnregisterListener 注销配置监听器 func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType) { - cns.mu.Lock() - defer cns.mu.Unlock() - - if listener, exists := cns.listeners[changeType]; exists { - cns.cleanupListener(listener) - delete(cns.listeners, changeType) - cns.logger.Info("ConfigNotification: Unregistered listener", "type", string(changeType)) + if value, loaded := cns.listeners.LoadAndDelete(changeType); loaded { + if listener, ok := value.(*ConfigListener); ok { + listener.cancel() + } } } // CheckConfigChanges 检查配置变更并通知相关监听器 func (cns *ConfigNotificationService) CheckConfigChanges() { - cns.mu.RLock() - listeners := make([]*ConfigListener, 0, len(cns.listeners)) - for _, listener := range cns.listeners { - listeners = append(listeners, listener) - } - cns.mu.RUnlock() - - // 检查每个监听器的配置变更 - for _, listener := range listeners { - if hasChanges, oldConfig, newConfig := cns.checkListenerConfigChanges(listener); hasChanges { - cns.logger.Debug("ConfigNotification: Actual config change detected", - "type", string(listener.ChangeType), - "listener", listener.Name) - - // 触发防抖通知,传递新旧配置 - cns.debounceNotifyWithChanges(listener, oldConfig, newConfig) + cns.listeners.Range(func(key, value interface{}) bool { + if listener, ok := value.(*ConfigListener); ok { + cns.checkAndNotify(listener) } - } + return true + }) } -// checkListenerConfigChanges 检查单个监听器的配置变更 -func (cns *ConfigNotificationService) checkListenerConfigChanges(listener *ConfigListener) (bool, interface{}, interface{}) { +// checkAndNotify 检查配置变更并通知 +func (cns *ConfigNotificationService) checkAndNotify(listener *ConfigListener) { if listener.GetConfigFunc == nil { - return false, nil, nil + return } - // 获取当前配置 currentConfig := listener.GetConfigFunc(cns.viper) - // 读取监听器状态 listener.mu.RLock() lastHash := listener.lastConfigHash lastConfig := listener.lastConfig listener.mu.RUnlock() - if currentConfig == nil { - // 配置不存在或获取失败 - if lastConfig != nil { - // 配置被删除,更新状态 - listener.mu.Lock() - listener.lastConfig = nil - listener.lastConfigHash = "" - listener.mu.Unlock() - return true, lastConfig, nil - } - return false, nil, nil + var hasChanges bool + var currentHash string + + if currentConfig != nil { + currentHash = computeConfigHash(currentConfig) + hasChanges = currentHash != lastHash + } else { + hasChanges = lastConfig != nil } - // 计算当前配置的哈希 - currentHash := cns.computeConfigHash(currentConfig) - - // 检查是否有变更 - if currentHash != lastHash { - // 更新监听器状态 + if hasChanges { listener.mu.Lock() - listener.lastConfig = cns.deepCopy(currentConfig) + listener.lastConfig = deepCopyConfig(currentConfig) listener.lastConfigHash = currentHash listener.mu.Unlock() - return true, lastConfig, currentConfig + cns.debounceNotify(listener, lastConfig, currentConfig) } - - return false, nil, nil } -// computeConfigHash 计算配置的哈希值 - 安全稳定版本 -func (cns *ConfigNotificationService) computeConfigHash(config interface{}) string { +// computeConfigHash 计算配置的哈希值 +func computeConfigHash(config *models.AppConfig) string { if config == nil { return "" } - // 使用JSON序列化确保稳定性和准确性 jsonBytes, err := json.Marshal(config) if err != nil { - // 如果JSON序列化失败,回退到字符串表示 - cns.logger.Warning("ConfigNotification: JSON marshal failed, using string representation", - "error", err) - configStr := fmt.Sprintf("%+v", config) - jsonBytes = []byte(configStr) + return fmt.Sprintf("%p", config) } hash := sha256.Sum256(jsonBytes) return fmt.Sprintf("%x", hash) } -// deepCopy 深拷贝配置对象 - 完整实现 -func (cns *ConfigNotificationService) deepCopy(src interface{}) interface{} { +// deepCopyConfig 深拷贝配置对象 +func deepCopyConfig(src *models.AppConfig) *models.AppConfig { if src == nil { return nil } - // 首先尝试JSON序列化方式深拷贝(适用于大多数配置对象) jsonBytes, err := json.Marshal(src) if err != nil { - cns.logger.Warning("ConfigNotification: JSON marshal for deep copy failed, using reflection", - "error", err) - return cns.reflectDeepCopy(src) - } - - // 创建同类型的新对象 - srcType := reflect.TypeOf(src) - var dst interface{} - - if srcType.Kind() == reflect.Ptr { - // 对于指针类型,创建指向新对象的指针 - elemType := srcType.Elem() - newObj := reflect.New(elemType) - dst = newObj.Interface() - } else { - // 对于值类型,创建零值 - newObj := reflect.New(srcType) - dst = newObj.Interface() - } - - // 反序列化到新对象 - err = json.Unmarshal(jsonBytes, dst) - if err != nil { - cns.logger.Warning("ConfigNotification: JSON unmarshal for deep copy failed, using reflection", - "error", err) - return cns.reflectDeepCopy(src) - } - - // 如果原对象不是指针类型,返回值而不是指针 - if srcType.Kind() != reflect.Ptr { - return reflect.ValueOf(dst).Elem().Interface() - } - - return dst -} - -// reflectDeepCopy 使用反射进行深拷贝的备用方法 -func (cns *ConfigNotificationService) reflectDeepCopy(src interface{}) interface{} { - srcValue := reflect.ValueOf(src) - return cns.reflectDeepCopyValue(srcValue).Interface() -} - -// reflectDeepCopyValue 递归深拷贝reflect.Value -func (cns *ConfigNotificationService) reflectDeepCopyValue(src reflect.Value) reflect.Value { - if !src.IsValid() { - return reflect.Value{} - } - - switch src.Kind() { - case reflect.Ptr: - if src.IsNil() { - return reflect.New(src.Type()).Elem() - } - dst := reflect.New(src.Type().Elem()) - dst.Elem().Set(cns.reflectDeepCopyValue(src.Elem())) - return dst - - case reflect.Struct: - dst := reflect.New(src.Type()).Elem() - for i := 0; i < src.NumField(); i++ { - if dst.Field(i).CanSet() { - dst.Field(i).Set(cns.reflectDeepCopyValue(src.Field(i))) - } - } - return dst - - case reflect.Slice: - if src.IsNil() { - return reflect.Zero(src.Type()) - } - dst := reflect.MakeSlice(src.Type(), src.Len(), src.Cap()) - for i := 0; i < src.Len(); i++ { - dst.Index(i).Set(cns.reflectDeepCopyValue(src.Index(i))) - } - return dst - - case reflect.Map: - if src.IsNil() { - return reflect.Zero(src.Type()) - } - dst := reflect.MakeMap(src.Type()) - for _, key := range src.MapKeys() { - dst.SetMapIndex(key, cns.reflectDeepCopyValue(src.MapIndex(key))) - } - return dst - - default: return src } + + var dst models.AppConfig + if err := json.Unmarshal(jsonBytes, &dst); err != nil { + return src + } + + return &dst } -// debounceNotifyWithChanges 防抖通知(带变更信息)- 修复内存泄漏 -func (cns *ConfigNotificationService) debounceNotifyWithChanges(listener *ConfigListener, oldConfig, newConfig interface{}) { +// debounceNotify 防抖通知 +func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) { listener.mu.Lock() defer listener.mu.Unlock() @@ -340,87 +197,61 @@ func (cns *ConfigNotificationService) debounceNotifyWithChanges(listener *Config } // 创建配置副本,避免在闭包中持有原始引用 - oldConfigCopy := cns.deepCopy(oldConfig) - newConfigCopy := cns.deepCopy(newConfig) + oldConfigCopy := deepCopyConfig(oldConfig) + newConfigCopy := deepCopyConfig(newConfig) - // 获取监听器信息的副本 - listenerName := listener.Name changeType := listener.ChangeType - stopChan := listener.stopChan - // 设置新的防抖定时器 listener.timer = time.AfterFunc(listener.DebounceDelay, func() { - cns.logger.Debug("ConfigNotification: Executing callback after debounce", - "listener", listenerName, - "type", string(changeType)) + cns.executeCallback(listener.ctx, changeType, listener.Callback, oldConfigCopy, newConfigCopy) + }) +} - // 启动独立的goroutine处理回调,带有超时和停止信号检查 - go func() { - defer func() { - if r := recover(); r != nil { - cns.logger.Error("ConfigNotification: Callback panic recovered", - "listener", listenerName, - "type", string(changeType), - "panic", r) - } - }() - - // 检查是否收到停止信号 - select { - case <-stopChan: - cns.logger.Debug("ConfigNotification: Callback cancelled due to stop signal", - "listener", listenerName) - return - default: - } - - // 执行回调,设置超时 - callbackDone := make(chan error, 1) - go func() { - callbackDone <- listener.Callback(changeType, oldConfigCopy, newConfigCopy) - }() - - select { - case <-stopChan: - cns.logger.Debug("ConfigNotification: Callback interrupted by stop signal", - "listener", listenerName) - return - case err := <-callbackDone: - if err != nil { - cns.logger.Error("ConfigNotification: Callback execution failed", - "listener", listenerName, - "type", string(changeType), - "error", err) - } else { - cns.logger.Debug("ConfigNotification: Callback executed successfully", - "listener", listenerName, - "type", string(changeType)) - } - case <-time.After(30 * time.Second): // 30秒超时 - cns.logger.Error("ConfigNotification: Callback execution timeout", - "listener", listenerName, - "type", string(changeType), - "timeout", "30s") +// executeCallback 执行回调函数 +func (cns *ConfigNotificationService) executeCallback( + ctx context.Context, + changeType ConfigChangeType, + callback ConfigChangeCallback, + oldConfig, newConfig *models.AppConfig, +) { + cns.wg.Add(1) + go func() { + defer cns.wg.Done() + defer func() { + if r := recover(); r != nil { + cns.logger.Error("config callback panic", "error", r) } }() - }) - cns.logger.Debug("ConfigNotification: Debounce timer scheduled", - "listener", listenerName, - "delay", listener.DebounceDelay) + callbackCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + done := make(chan error, 1) + go func() { + done <- callback(changeType, oldConfig, newConfig) + }() + + select { + case <-callbackCtx.Done(): + cns.logger.Error("config callback timeout") + case err := <-done: + if err != nil { + cns.logger.Error("config callback error", "error", err) + } + } + }() } // Cleanup 清理所有监听器 func (cns *ConfigNotificationService) Cleanup() { - cns.mu.Lock() - defer cns.mu.Unlock() + cns.cancel() // 取消所有context - for changeType, listener := range cns.listeners { - cns.cleanupListener(listener) - delete(cns.listeners, changeType) - } + cns.listeners.Range(func(key, value interface{}) bool { + cns.listeners.Delete(key) + return true + }) - cns.logger.Info("ConfigNotification: All listeners cleaned up") + cns.wg.Wait() // 等待所有协程完成 } // CreateHotkeyListener 创建热键配置监听器 @@ -428,20 +259,16 @@ func CreateHotkeyListener(callback func(enable bool, hotkey *models.HotkeyCombo) return &ConfigListener{ Name: "HotkeyListener", ChangeType: ConfigChangeTypeHotkey, - Callback: func(changeType ConfigChangeType, oldConfig, newConfig interface{}) error { - // 处理新配置 - if newAppConfig, ok := newConfig.(*models.AppConfig); ok { - return callback(newAppConfig.General.EnableGlobalHotkey, &newAppConfig.General.GlobalHotkey) + Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { + if newConfig != nil { + return callback(newConfig.General.EnableGlobalHotkey, &newConfig.General.GlobalHotkey) } - // 如果新配置为空,说明配置被删除,使用默认值 - if newConfig == nil { - defaultConfig := models.NewDefaultAppConfig() - return callback(defaultConfig.General.EnableGlobalHotkey, &defaultConfig.General.GlobalHotkey) - } - return nil + // 使用默认配置 + defaultConfig := models.NewDefaultAppConfig() + return callback(defaultConfig.General.EnableGlobalHotkey, &defaultConfig.General.GlobalHotkey) }, DebounceDelay: 200 * time.Millisecond, - GetConfigFunc: func(v *viper.Viper) interface{} { + GetConfigFunc: func(v *viper.Viper) *models.AppConfig { var config models.AppConfig if err := v.Unmarshal(&config); err != nil { return nil @@ -456,31 +283,27 @@ func CreateDataPathListener(callback func(oldPath, newPath string) error) *Confi return &ConfigListener{ Name: "DataPathListener", ChangeType: ConfigChangeTypeDataPath, - Callback: func(changeType ConfigChangeType, oldConfig, newConfig interface{}) error { + Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { var oldPath, newPath string - // 处理旧配置 - if oldAppConfig, ok := oldConfig.(*models.AppConfig); ok { - oldPath = oldAppConfig.General.DataPath + if oldConfig != nil { + oldPath = oldConfig.General.DataPath } - // 处理新配置 - if newAppConfig, ok := newConfig.(*models.AppConfig); ok { - newPath = newAppConfig.General.DataPath - } else if newConfig == nil { - // 如果新配置为空,说明配置被删除,使用默认值 + if newConfig != nil { + newPath = newConfig.General.DataPath + } else { defaultConfig := models.NewDefaultAppConfig() newPath = defaultConfig.General.DataPath } - // 只有路径真正改变时才调用回调 if oldPath != newPath { return callback(oldPath, newPath) } return nil }, - DebounceDelay: 100 * time.Millisecond, // 较短的防抖延迟,因为数据路径变更需要快速响应 - GetConfigFunc: func(v *viper.Viper) interface{} { + DebounceDelay: 100 * time.Millisecond, + GetConfigFunc: func(v *viper.Viper) *models.AppConfig { var config models.AppConfig if err := v.Unmarshal(&config); err != nil { return nil diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 1f9828e..3906a34 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "path/filepath" "sync" "time" "voidraft/internal/models" @@ -17,9 +16,10 @@ import ( // ConfigService 提供基于 Viper 的配置管理功能 type ConfigService struct { - viper *viper.Viper // Viper 实例 - logger *log.LoggerService // 日志服务 - mu sync.RWMutex // 读写锁 + viper *viper.Viper // Viper 实例 + logger *log.LoggerService // 日志服务 + pathManager *PathManager // 路径管理器 + mu sync.RWMutex // 读写锁 // 配置通知服务 notificationService *ConfigNotificationService @@ -49,29 +49,24 @@ func (e *ConfigError) Is(target error) bool { } // NewConfigService 创建新的配置服务实例 -func NewConfigService(logger *log.LoggerService) *ConfigService { +func NewConfigService(logger *log.LoggerService, pathManager *PathManager) *ConfigService { // 设置日志服务 if logger == nil { logger = log.New() } - // 获取当前工作目录 - currentDir, err := os.Getwd() - if err != nil { - currentDir = "." + // 设置路径管理器 + if pathManager == nil { + pathManager = NewPathManager() } - // 固定配置路径和文件名 - configPath := filepath.Join(currentDir, "config") - configName := "settings" - // 创建 Viper 实例 v := viper.New() // 配置 Viper - v.SetConfigName(configName) + v.SetConfigName(pathManager.GetConfigName()) v.SetConfigType("json") - v.AddConfigPath(configPath) + v.AddConfigPath(pathManager.GetConfigDir()) // 设置环境变量前缀 v.SetEnvPrefix("VOIDRAFT") @@ -82,8 +77,9 @@ func NewConfigService(logger *log.LoggerService) *ConfigService { // 构造配置服务实例 service := &ConfigService{ - viper: v, - logger: logger, + viper: v, + logger: logger, + pathManager: pathManager, } // 初始化配置通知服务 @@ -91,7 +87,7 @@ func NewConfigService(logger *log.LoggerService) *ConfigService { // 初始化配置 if err := service.initConfig(); err != nil { - service.logger.Error("Config: Failed to initialize config", "error", err) + service.logger.Error("Failed to initialize config", "error", err) } // 启动配置文件监听 @@ -143,29 +139,19 @@ func (cs *ConfigService) initConfig() error { var configFileNotFoundError viper.ConfigFileNotFoundError if errors.As(err, &configFileNotFoundError) { // 配置文件不存在,创建默认配置文件 - cs.logger.Info("Config: Config file not found, creating default config") return cs.createDefaultConfig() } // 配置文件存在但读取失败 return &ConfigError{Operation: "read_config", Err: err} } - cs.logger.Info("Config: Successfully loaded config file", "file", cs.viper.ConfigFileUsed()) return nil } // createDefaultConfig 创建默认配置文件 func (cs *ConfigService) createDefaultConfig() error { - // 获取配置目录路径 - currentDir, err := os.Getwd() - if err != nil { - currentDir = "." - } - configDir := filepath.Join(currentDir, "config") - configPath := filepath.Join(configDir, "settings.json") - // 确保配置目录存在 - if err := os.MkdirAll(configDir, 0755); err != nil { + if err := cs.pathManager.EnsureConfigDir(); err != nil { return &ConfigError{Operation: "create_config_dir", Err: err} } @@ -179,7 +165,7 @@ func (cs *ConfigService) createDefaultConfig() error { } // 写入配置文件 - if err := os.WriteFile(configPath, configBytes, 0644); err != nil { + if err := os.WriteFile(cs.pathManager.GetSettingsPath(), configBytes, 0644); err != nil { return &ConfigError{Operation: "write_default_config", Err: err} } @@ -188,7 +174,6 @@ func (cs *ConfigService) createDefaultConfig() error { return &ConfigError{Operation: "read_created_config", Err: err} } - cs.logger.Info("Config: Created default config file", "path", configPath) return nil } @@ -196,15 +181,12 @@ func (cs *ConfigService) createDefaultConfig() error { func (cs *ConfigService) startWatching() { // 设置配置变化回调 cs.viper.OnConfigChange(func(e fsnotify.Event) { - cs.logger.Info("Config: Config file changed", "file", e.Name, "operation", e.Op.String()) - // 使用配置通知服务检查所有已注册的配置变更 cs.notificationService.CheckConfigChanges() }) // 启动配置文件监听 cs.viper.WatchConfig() - cs.logger.Info("Config: Started watching config file for changes") } // GetConfig 获取完整应用配置 @@ -261,30 +243,20 @@ func (cs *ConfigService) ResetConfig() { // 直接写入JSON文件 if err := cs.writeConfigToFile(defaultConfig); err != nil { - cs.logger.Error("Config: Failed to write config during reset", "error", err) return } // 重新读取配置文件到viper if err := cs.viper.ReadInConfig(); err != nil { - cs.logger.Error("Config: Failed to reload config after reset", "error", err) return } - cs.logger.Info("Config: All settings have been reset to defaults") // 手动触发配置变更检查,确保通知系统能感知到变更 cs.notificationService.CheckConfigChanges() } // writeConfigToFile 直接写入配置到JSON文件 func (cs *ConfigService) writeConfigToFile(config *models.AppConfig) error { - // 获取配置文件路径 - currentDir, err := os.Getwd() - if err != nil { - currentDir = "." - } - configPath := filepath.Join(currentDir, "config", "settings.json") - // 序列化为JSON configBytes, err := json.MarshalIndent(config, "", " ") if err != nil { @@ -292,7 +264,7 @@ func (cs *ConfigService) writeConfigToFile(config *models.AppConfig) error { } // 写入文件 - if err := os.WriteFile(configPath, configBytes, 0644); err != nil { + if err := os.WriteFile(cs.pathManager.GetSettingsPath(), configBytes, 0644); err != nil { return fmt.Errorf("failed to write config file: %v", err) } diff --git a/internal/services/dialog_service.go b/internal/services/dialog_service.go index 743800e..0441162 100644 --- a/internal/services/dialog_service.go +++ b/internal/services/dialog_service.go @@ -26,7 +26,6 @@ func NewDialogService(logger *log.LoggerService) *DialogService { // SetWindow 设置绑定的窗口 func (ds *DialogService) SetWindow(window *application.WebviewWindow) { ds.window = window - ds.logger.Info("Dialog service window binding updated") } // SelectDirectory 打开目录选择对话框 @@ -65,10 +64,7 @@ func (ds *DialogService) SelectDirectory() (string, error) { path, err := dialog.PromptForSingleSelection() if err != nil { - ds.logger.Error("Failed to select directory", "error", err) return "", err } - - ds.logger.Info("Directory selected", "path", path) return path, nil } diff --git a/internal/services/document_service.go b/internal/services/document_service.go index cadcfb6..ef9da32 100644 --- a/internal/services/document_service.go +++ b/internal/services/document_service.go @@ -1,9 +1,11 @@ package services import ( + "context" "os" "path/filepath" "sync" + "sync/atomic" "time" "voidraft/internal/models" @@ -14,15 +16,21 @@ import ( type DocumentService struct { configService *ConfigService logger *log.LoggerService - document *models.Document docStore *Store[models.Document] - mutex sync.RWMutex - // 自动保存优化 - saveTimer *time.Timer - isDirty bool - lastSaveTime time.Time - pendingContent string // 暂存待保存的内容 + // 文档状态管理 + mu sync.RWMutex + document *models.Document + + // 自动保存管理 + ctx context.Context + cancel context.CancelFunc + isDirty atomic.Bool + lastSaveTime atomic.Int64 // unix timestamp + saveScheduler chan struct{} + + // 初始化控制 + initOnce sync.Once } // NewDocumentService 创建文档服务 @@ -31,20 +39,33 @@ func NewDocumentService(configService *ConfigService, logger *log.LoggerService) logger = log.New() } + ctx, cancel := context.WithCancel(context.Background()) return &DocumentService{ configService: configService, logger: logger, + ctx: ctx, + cancel: cancel, + saveScheduler: make(chan struct{}, 1), } } // Initialize 初始化服务 func (ds *DocumentService) Initialize() error { + var initErr error + ds.initOnce.Do(func() { + initErr = ds.doInitialize() + }) + return initErr +} + +// doInitialize 执行初始化 +func (ds *DocumentService) doInitialize() error { if err := ds.initStore(); err != nil { return err } ds.loadDocument() - ds.startAutoSave() + go ds.autoSaveWorker() return nil } @@ -55,7 +76,6 @@ func (ds *DocumentService) initStore() error { return err } - // 确保目录存在 if err := os.MkdirAll(filepath.Dir(docPath), 0755); err != nil { return err } @@ -75,72 +95,75 @@ func (ds *DocumentService) getDocumentPath() (string, error) { if err != nil { return "", err } - return filepath.Join(config.General.DataPath, "docs", "default.json"), nil } // loadDocument 加载文档 func (ds *DocumentService) loadDocument() { - ds.mutex.Lock() - defer ds.mutex.Unlock() + ds.mu.Lock() + defer ds.mu.Unlock() doc := ds.docStore.Get() if doc.Meta.ID == "" { - // 创建新文档 ds.document = models.NewDefaultDocument() ds.docStore.Set(*ds.document) - ds.logger.Info("Document: Created new document") } else { ds.document = &doc - ds.logger.Info("Document: Loaded existing document") } + + ds.lastSaveTime.Store(time.Now().Unix()) } // GetActiveDocument 获取活动文档 func (ds *DocumentService) GetActiveDocument() (*models.Document, error) { - ds.mutex.RLock() - defer ds.mutex.RUnlock() + ds.mu.RLock() + defer ds.mu.RUnlock() if ds.document == nil { return nil, nil } - // 返回副本 docCopy := *ds.document return &docCopy, nil } // UpdateActiveDocumentContent 更新文档内容 func (ds *DocumentService) UpdateActiveDocumentContent(content string) error { - ds.mutex.Lock() - defer ds.mutex.Unlock() + ds.mu.Lock() + defer ds.mu.Unlock() - if ds.document != nil { - // 只在内容真正改变时才标记为脏 - if ds.document.Content != content { - ds.pendingContent = content - ds.isDirty = true - } + if ds.document != nil && ds.document.Content != content { + ds.document.Content = content + ds.markDirty() } return nil } +// markDirty 标记为脏数据并触发自动保存 +func (ds *DocumentService) markDirty() { + if ds.isDirty.CompareAndSwap(false, true) { + select { + case ds.saveScheduler <- struct{}{}: + default: // 已有保存任务在队列中 + } + } +} + // ForceSave 强制保存 func (ds *DocumentService) ForceSave() error { - ds.mutex.Lock() - defer ds.mutex.Unlock() + return ds.saveDocument() +} + +// saveDocument 保存文档 +func (ds *DocumentService) saveDocument() error { + ds.mu.Lock() + defer ds.mu.Unlock() if ds.document == nil { return nil } - // 应用待保存的内容 - if ds.pendingContent != "" { - ds.document.Content = ds.pendingContent - ds.pendingContent = "" - } - now := time.Now() ds.document.Meta.LastUpdated = now @@ -152,85 +175,71 @@ func (ds *DocumentService) ForceSave() error { return err } - ds.isDirty = false - ds.lastSaveTime = now - ds.logger.Info("Document: Force save completed") + ds.isDirty.Store(false) + ds.lastSaveTime.Store(now.Unix()) return nil } -// startAutoSave 启动自动保存 -func (ds *DocumentService) startAutoSave() { - delay := 5 * time.Second // 默认延迟 +// autoSaveWorker 自动保存工作协程 +func (ds *DocumentService) autoSaveWorker() { + ticker := time.NewTicker(ds.getAutoSaveInterval()) + defer ticker.Stop() - if config, err := ds.configService.GetConfig(); err == nil { - delay = time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond + for { + select { + case <-ds.ctx.Done(): + return + case <-ds.saveScheduler: + ds.performAutoSave() + case <-ticker.C: + if ds.isDirty.Load() { + ds.performAutoSave() + } + // 动态调整保存间隔 + ticker.Reset(ds.getAutoSaveInterval()) + } } - - ds.scheduleAutoSave(delay) } -// scheduleAutoSave 安排自动保存 -func (ds *DocumentService) scheduleAutoSave(delay time.Duration) { - if ds.saveTimer != nil { - ds.saveTimer.Stop() +// getAutoSaveInterval 获取自动保存间隔 +func (ds *DocumentService) getAutoSaveInterval() time.Duration { + config, err := ds.configService.GetConfig() + if err != nil { + return 5 * time.Second } - - ds.saveTimer = time.AfterFunc(delay, func() { - ds.performAutoSave() - ds.startAutoSave() // 重新安排 - }) + return time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond } // performAutoSave 执行自动保存 func (ds *DocumentService) performAutoSave() { - ds.mutex.Lock() - defer ds.mutex.Unlock() - - if !ds.isDirty || ds.document == nil { + if !ds.isDirty.Load() { return } - // 检查距离上次保存的时间间隔,避免过于频繁的保存 - now := time.Now() - if now.Sub(ds.lastSaveTime) < time.Second { - // 如果距离上次保存不到1秒,跳过此次保存 - // 下一个自动保存周期会重新尝试 - ds.logger.Debug("Document: Skipping auto save due to recent save") + // 防抖:避免过于频繁的保存 + lastSave := time.Unix(ds.lastSaveTime.Load(), 0) + if time.Since(lastSave) < time.Second { + // 延迟重试 + time.AfterFunc(time.Second, func() { + select { + case ds.saveScheduler <- struct{}{}: + default: + } + }) return } - // 应用待保存的内容 - if ds.pendingContent != "" { - ds.document.Content = ds.pendingContent - ds.pendingContent = "" + if err := ds.saveDocument(); err != nil { + ds.logger.Error("auto save failed", "error", err) } - - ds.document.Meta.LastUpdated = now - - if err := ds.docStore.Set(*ds.document); err != nil { - ds.logger.Error("Document: Auto save failed", "error", err) - return - } - - if err := ds.docStore.Save(); err != nil { - ds.logger.Error("Document: Auto save failed", "error", err) - return - } - - ds.isDirty = false - ds.lastSaveTime = now - ds.logger.Debug("Document: Auto save completed") } // ReloadDocument 重新加载文档 func (ds *DocumentService) ReloadDocument() error { - ds.mutex.Lock() - defer ds.mutex.Unlock() - - // 强制保存当前文档 - if ds.document != nil && ds.isDirty { - if err := ds.forceSaveInternal(); err != nil { - ds.logger.Error("Document: Failed to save before reload", "error", err) + // 先保存当前文档 + if ds.isDirty.Load() { + if err := ds.saveDocument(); err != nil { + return err } } @@ -239,76 +248,24 @@ func (ds *DocumentService) ReloadDocument() error { return err } - // 重新加载文档 - doc := ds.docStore.Get() - if doc.Meta.ID == "" { - // 创建新文档 - ds.document = models.NewDefaultDocument() - ds.docStore.Set(*ds.document) - ds.logger.Info("Document: Created new document after reload") - } else { - ds.document = &doc - ds.logger.Info("Document: Loaded existing document after reload") - } - - // 重置状态 - ds.isDirty = false - ds.pendingContent = "" - ds.lastSaveTime = time.Now() - - return nil -} - -// forceSaveInternal 内部强制保存(不加锁) -func (ds *DocumentService) forceSaveInternal() error { - if ds.document == nil { - return nil - } - - // 应用待保存的内容 - if ds.pendingContent != "" { - ds.document.Content = ds.pendingContent - ds.pendingContent = "" - } - - now := time.Now() - ds.document.Meta.LastUpdated = now - - if err := ds.docStore.Set(*ds.document); err != nil { - return err - } - - if err := ds.docStore.Save(); err != nil { - return err - } - - ds.isDirty = false - ds.lastSaveTime = now + // 重新加载 + ds.loadDocument() return nil } // ServiceShutdown 关闭服务 func (ds *DocumentService) ServiceShutdown() error { - // 停止定时器 - if ds.saveTimer != nil { - ds.saveTimer.Stop() - } + ds.cancel() // 停止自动保存工作协程 // 最后保存 - if err := ds.ForceSave(); err != nil { - ds.logger.Error("Document: Failed to save on shutdown", "error", err) + if ds.isDirty.Load() { + return ds.saveDocument() } - ds.logger.Info("Document: Service shutdown completed") return nil } // OnDataPathChanged 处理数据路径变更 func (ds *DocumentService) OnDataPathChanged(oldPath, newPath string) error { - ds.logger.Info("Document: Data path changed, reloading document", - "oldPath", oldPath, - "newPath", newPath) - - // 重新加载文档以使用新的路径 return ds.ReloadDocument() } diff --git a/internal/services/hotkey_service.go b/internal/services/hotkey_service.go index 96925f4..901af5c 100644 --- a/internal/services/hotkey_service.go +++ b/internal/services/hotkey_service.go @@ -11,8 +11,10 @@ package services import "C" import ( + "context" "fmt" "sync" + "sync/atomic" "time" "voidraft/internal/models" @@ -20,17 +22,19 @@ import ( "github.com/wailsapp/wails/v3/pkg/services/log" ) -// HotkeyService 全局热键服务 +// HotkeyService Windows全局热键服务 type HotkeyService struct { logger *log.LoggerService configService *ConfigService app *application.App + mu sync.RWMutex - isRegistered bool currentHotkey *models.HotkeyCombo - stopChan chan struct{} - wg sync.WaitGroup - running bool + isRegistered atomic.Bool + + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup } // HotkeyError 热键错误 @@ -39,27 +43,26 @@ type HotkeyError struct { Err error } -// Error 实现error接口 func (e *HotkeyError) Error() string { - return fmt.Sprintf("hotkey error during %s: %v", e.Operation, e.Err) + return fmt.Sprintf("hotkey %s: %v", e.Operation, e.Err) } -// Unwrap 获取原始错误 func (e *HotkeyError) Unwrap() error { return e.Err } -// NewHotkeyService 创建新的热键服务实例 +// NewHotkeyService 创建热键服务实例 func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService { if logger == nil { logger = log.New() } + ctx, cancel := context.WithCancel(context.Background()) return &HotkeyService{ logger: logger, configService: configService, - isRegistered: false, - running: false, + ctx: ctx, + cancel: cancel, } } @@ -67,72 +70,57 @@ func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) * func (hs *HotkeyService) Initialize(app *application.App) error { hs.app = app - // 加载并应用当前配置 config, err := hs.configService.GetConfig() if err != nil { - return &HotkeyError{Operation: "load_config", Err: err} + return &HotkeyError{"load_config", err} } if config.General.EnableGlobalHotkey { - err = hs.RegisterHotkey(&config.General.GlobalHotkey) - if err != nil { - hs.logger.Error("Hotkey: Failed to register hotkey on startup", "error", err) + if err := hs.RegisterHotkey(&config.General.GlobalHotkey); err != nil { + hs.logger.Error("failed to register startup hotkey", "error", err) } } - hs.logger.Info("Hotkey: Service initialized") return nil } // RegisterHotkey 注册全局热键 func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { + if !hs.isValidHotkey(hotkey) { + return &HotkeyError{"validate", fmt.Errorf("invalid hotkey combination")} + } + hs.mu.Lock() defer hs.mu.Unlock() - // 先取消注册现有热键 - if hs.isRegistered { - hs.logger.Info("Hotkey: Unregistering existing hotkey before registering new one") - err := hs.unregisterHotkeyInternal() - if err != nil { - return err - } + // 取消现有热键 + if hs.isRegistered.Load() { + hs.unregisterInternal() } - // 验证热键组合 - if !hs.isValidHotkey(hotkey) { - return &HotkeyError{Operation: "validate_hotkey", Err: fmt.Errorf("invalid hotkey combination")} - } - - hs.logger.Info("Hotkey: Registering global hotkey using Windows API", - "ctrl", hotkey.Ctrl, - "shift", hotkey.Shift, - "alt", hotkey.Alt, - "win", hotkey.Win, - "key", hotkey.Key) - - // 创建ready channel等待goroutine启动完成 - readyChan := make(chan struct{}) - - // 确保 stopChan 是新的 - hs.stopChan = make(chan struct{}) + // 启动监听器 + ctx, cancel := context.WithCancel(hs.ctx) hs.wg.Add(1) - go hs.hotkeyListener(hotkey, readyChan) - // 等待监听器启动完成,设置超时避免无限等待 + ready := make(chan error, 1) + go hs.hotkeyListener(ctx, hotkey, ready) + + // 等待启动完成 select { - case <-readyChan: - // 监听器启动完成 - case <-time.After(1 * time.Second): - // 超时处理 - hs.logger.Warning("Hotkey: Timeout waiting for listener to start") - return &HotkeyError{Operation: "start_listener", Err: fmt.Errorf("timeout waiting for hotkey listener to start")} + case err := <-ready: + if err != nil { + cancel() + return &HotkeyError{"register", err} + } + case <-time.After(time.Second): + cancel() + return &HotkeyError{"register", fmt.Errorf("timeout")} } hs.currentHotkey = hotkey - hs.isRegistered = true - hs.running = true + hs.isRegistered.Store(true) + hs.cancel = cancel - hs.logger.Info("Hotkey: Successfully registered global hotkey") return nil } @@ -140,204 +128,85 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { func (hs *HotkeyService) UnregisterHotkey() error { hs.mu.Lock() defer hs.mu.Unlock() - - return hs.unregisterHotkeyInternal() + return hs.unregisterInternal() } -// unregisterHotkeyInternal 内部取消注册方法(无锁) -func (hs *HotkeyService) unregisterHotkeyInternal() error { - if !hs.isRegistered { - hs.logger.Debug("Hotkey: No hotkey registered, skipping unregister") +// unregisterInternal 内部取消注册(无锁) +func (hs *HotkeyService) unregisterInternal() error { + if !hs.isRegistered.Load() { return nil } - hs.logger.Info("Hotkey: Unregistering global hotkey") - - // 停止监听 - if hs.stopChan != nil { - close(hs.stopChan) - hs.logger.Debug("Hotkey: Waiting for listener goroutine to stop") + if hs.cancel != nil { + hs.cancel() hs.wg.Wait() - hs.logger.Debug("Hotkey: Listener goroutine stopped") } - // 重置状态 hs.currentHotkey = nil - hs.isRegistered = false - hs.running = false - hs.stopChan = nil - - hs.logger.Info("Hotkey: Successfully unregistered global hotkey") + hs.isRegistered.Store(false) return nil } // UpdateHotkey 更新热键配置 func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error { - hs.logger.Info("Hotkey: === UpdateHotkey called ===", - "enable", enable, - "ctrl", hotkey.Ctrl, - "shift", hotkey.Shift, - "alt", hotkey.Alt, - "win", hotkey.Win, - "key", hotkey.Key) - - // 先获取当前状态 - hs.mu.RLock() - currentRegistered := hs.isRegistered - var currentHotkey *models.HotkeyCombo - if hs.currentHotkey != nil { - currentHotkey = &models.HotkeyCombo{ - Ctrl: hs.currentHotkey.Ctrl, - Shift: hs.currentHotkey.Shift, - Alt: hs.currentHotkey.Alt, - Win: hs.currentHotkey.Win, - Key: hs.currentHotkey.Key, - } - } - hs.mu.RUnlock() - - hs.logger.Info("Hotkey: Current state", - "currentRegistered", currentRegistered, - "currentHotkey", currentHotkey) - - // 检查是否需要更新 - needsUpdate := false - if enable != currentRegistered { - needsUpdate = true - hs.logger.Info("Hotkey: Enable state changed", "old", currentRegistered, "new", enable) - } else if enable && currentHotkey != nil { - // 如果启用状态,检查热键组合是否变化 - if hotkey.Ctrl != currentHotkey.Ctrl || - hotkey.Shift != currentHotkey.Shift || - hotkey.Alt != currentHotkey.Alt || - hotkey.Win != currentHotkey.Win || - hotkey.Key != currentHotkey.Key { - needsUpdate = true - hs.logger.Info("Hotkey: Hotkey combination changed", - "old", fmt.Sprintf("Ctrl:%v Shift:%v Alt:%v Win:%v Key:%s", - currentHotkey.Ctrl, currentHotkey.Shift, currentHotkey.Alt, currentHotkey.Win, currentHotkey.Key), - "new", fmt.Sprintf("Ctrl:%v Shift:%v Alt:%v Win:%v Key:%s", - hotkey.Ctrl, hotkey.Shift, hotkey.Alt, hotkey.Win, hotkey.Key)) - } - } - - if !needsUpdate { - hs.logger.Info("Hotkey: No changes detected, skipping update") - return nil - } - - hs.logger.Info("Hotkey: Proceeding with hotkey update", "needsUpdate", needsUpdate) - if enable { - // 启用热键:直接注册新热键(RegisterHotkey 会处理取消旧热键) - err := hs.RegisterHotkey(hotkey) - if err != nil { - hs.logger.Error("Hotkey: Failed to register new hotkey", "error", err) - return err - } - hs.logger.Info("Hotkey: Successfully updated and registered new hotkey") - return nil - } else { - // 禁用热键:取消注册 - err := hs.UnregisterHotkey() - if err != nil { - hs.logger.Error("Hotkey: Failed to unregister hotkey", "error", err) - return err - } - hs.logger.Info("Hotkey: Successfully disabled hotkey") - return nil + return hs.RegisterHotkey(hotkey) } + return hs.UnregisterHotkey() } -// ToggleWindow 切换窗口显示/隐藏 - 通过事件通知前端处理 -func (hs *HotkeyService) ToggleWindow() { - if hs.app == nil { - hs.logger.Warning("Hotkey: App is nil, cannot toggle") - return - } - - // 发送事件到前端,让前端处理窗口切换 - hs.app.EmitEvent("hotkey:toggle-window", nil) - hs.logger.Debug("Hotkey: Emitted toggle window event") -} - -// hotkeyListener 热键监听器goroutine -func (hs *HotkeyService) hotkeyListener(hotkey *models.HotkeyCombo, readyChan chan struct{}) { +// hotkeyListener 热键监听器 +func (hs *HotkeyService) hotkeyListener(ctx context.Context, hotkey *models.HotkeyCombo, ready chan<- error) { defer hs.wg.Done() - hs.logger.Debug("Hotkey: Starting Windows API hotkey listener") - - // 将热键转换为虚拟键码 mainKeyVK := hs.keyToVirtualKeyCode(hotkey.Key) if mainKeyVK == 0 { - hs.logger.Error("Hotkey: Invalid key", "key", hotkey.Key) - close(readyChan) + ready <- fmt.Errorf("invalid key: %s", hotkey.Key) return } - // 检查间隔(100ms,减少CPU使用率) ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() - // 添加状态跟踪 - var wasPressed bool = false - - // 标记是否已经发送ready信号,确保只发送一次 - readySent := false + var wasPressed bool + ready <- nil // 标记准备就绪 for { select { - case <-hs.stopChan: - hs.logger.Debug("Hotkey: Stopping Windows API hotkey listener") - if !readySent { - close(readyChan) - } + case <-ctx.Done(): return case <-ticker.C: - // 第一次循环时发送ready信号,表示监听器已经准备就绪 - if !readySent { - close(readyChan) - readySent = true - hs.logger.Debug("Hotkey: Listener ready signal sent") - } + ctrl := cBool(hotkey.Ctrl) + shift := cBool(hotkey.Shift) + alt := cBool(hotkey.Alt) + win := cBool(hotkey.Win) - // 调用 C 函数检查热键组合 - ctrl := 0 - if hotkey.Ctrl { - ctrl = 1 - } + isPressed := C.isHotkeyPressed(ctrl, shift, alt, win, C.int(mainKeyVK)) == 1 - shift := 0 - if hotkey.Shift { - shift = 1 - } - - alt := 0 - if hotkey.Alt { - alt = 1 - } - - win := 0 - if hotkey.Win { - win = 1 - } - - // 检查热键是否被按下 - isPressed := int(C.isHotkeyPressed(C.int(ctrl), C.int(shift), C.int(alt), C.int(win), C.int(mainKeyVK))) == 1 - - // 只在按键从未按下变为按下时触发(边缘触发) if isPressed && !wasPressed { - hs.logger.Debug("Hotkey: Global hotkey triggered via Windows API") - hs.ToggleWindow() + hs.toggleWindow() } - - // 更新状态 wasPressed = isPressed } } } -// keyToVirtualKeyCode 将键名转换为 Windows 虚拟键码 +// cBool 转换Go bool为C int +func cBool(b bool) C.int { + if b { + return 1 + } + return 0 +} + +// toggleWindow 切换窗口 +func (hs *HotkeyService) toggleWindow() { + if hs.app != nil { + hs.app.EmitEvent("hotkey:toggle-window", nil) + } +} + +// keyToVirtualKeyCode 键名转虚拟键码 func (hs *HotkeyService) keyToVirtualKeyCode(key string) int { keyMap := map[string]int{ // 字母键 @@ -345,43 +214,29 @@ func (hs *HotkeyService) keyToVirtualKeyCode(key string) int { "I": 0x49, "J": 0x4A, "K": 0x4B, "L": 0x4C, "M": 0x4D, "N": 0x4E, "O": 0x4F, "P": 0x50, "Q": 0x51, "R": 0x52, "S": 0x53, "T": 0x54, "U": 0x55, "V": 0x56, "W": 0x57, "X": 0x58, "Y": 0x59, "Z": 0x5A, - // 数字键 "0": 0x30, "1": 0x31, "2": 0x32, "3": 0x33, "4": 0x34, "5": 0x35, "6": 0x36, "7": 0x37, "8": 0x38, "9": 0x39, - // 功能键 "F1": 0x70, "F2": 0x71, "F3": 0x72, "F4": 0x73, "F5": 0x74, "F6": 0x75, "F7": 0x76, "F8": 0x77, "F9": 0x78, "F10": 0x79, "F11": 0x7A, "F12": 0x7B, } - - if vk, exists := keyMap[key]; exists { - return vk - } - return 0 + return keyMap[key] } -// isValidHotkey 验证热键组合是否有效 +// isValidHotkey 验证热键组合 func (hs *HotkeyService) isValidHotkey(hotkey *models.HotkeyCombo) bool { - if hotkey == nil { + if hotkey == nil || hotkey.Key == "" { return false } - - // 必须有主键 - if hotkey.Key == "" { - return false - } - - // 必须至少有一个修饰键 + // 至少需要一个修饰键 if !hotkey.Ctrl && !hotkey.Shift && !hotkey.Alt && !hotkey.Win { return false } - - // 验证主键是否在有效范围内 return hs.keyToVirtualKeyCode(hotkey.Key) != 0 } -// GetCurrentHotkey 获取当前注册的热键 +// GetCurrentHotkey 获取当前热键 func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { hs.mu.RLock() defer hs.mu.RUnlock() @@ -390,7 +245,6 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { return nil } - // 返回副本避免并发问题 return &models.HotkeyCombo{ Ctrl: hs.currentHotkey.Ctrl, Shift: hs.currentHotkey.Shift, @@ -400,25 +254,14 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { } } -// IsRegistered 检查是否已注册热键 +// IsRegistered 检查是否已注册 func (hs *HotkeyService) IsRegistered() bool { - hs.mu.RLock() - defer hs.mu.RUnlock() - return hs.isRegistered + return hs.isRegistered.Load() } -// ServiceShutdown 关闭热键服务 +// ServiceShutdown 关闭服务 func (hs *HotkeyService) ServiceShutdown() error { - hs.mu.Lock() - defer hs.mu.Unlock() - - if hs.isRegistered { - err := hs.unregisterHotkeyInternal() - if err != nil { - hs.logger.Error("Hotkey: Failed to unregister hotkey on shutdown", "error", err) - } - } - - hs.logger.Info("Hotkey: Service shutdown completed") + hs.cancel() + hs.wg.Wait() return nil } diff --git a/internal/services/hotkey_service_darwin.go b/internal/services/hotkey_service_darwin.go index b64af8a..cf628b6 100644 --- a/internal/services/hotkey_service_darwin.go +++ b/internal/services/hotkey_service_darwin.go @@ -68,6 +68,7 @@ import "C" import ( "fmt" "sync" + "sync/atomic" "voidraft/internal/models" "github.com/wailsapp/wails/v3/pkg/application" @@ -83,7 +84,7 @@ type HotkeyService struct { configService *ConfigService app *application.App mu sync.RWMutex - isRegistered bool + isRegistered atomic.Bool currentHotkey *models.HotkeyCombo } @@ -95,7 +96,7 @@ type HotkeyError struct { // Error 实现error接口 func (e *HotkeyError) Error() string { - return fmt.Sprintf("hotkey error during %s: %v", e.Operation, e.Err) + return fmt.Sprintf("hotkey %s: %v", e.Operation, e.Err) } // Unwrap 获取原始错误 @@ -112,7 +113,6 @@ func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) * service := &HotkeyService{ logger: logger, configService: configService, - isRegistered: false, } // 设置全局实例 @@ -128,41 +128,31 @@ func (hs *HotkeyService) Initialize(app *application.App) error { // 加载并应用当前配置 config, err := hs.configService.GetConfig() if err != nil { - return &HotkeyError{Operation: "load_config", Err: err} + return &HotkeyError{"load_config", err} } if config.General.EnableGlobalHotkey { - err = hs.RegisterHotkey(&config.General.GlobalHotkey) - if err != nil { - hs.logger.Error("Hotkey: Failed to register hotkey on startup", "error", err) + if err := hs.RegisterHotkey(&config.General.GlobalHotkey); err != nil { + hs.logger.Error("failed to register startup hotkey", "error", err) } } - hs.logger.Info("Hotkey: macOS service initialized") return nil } // RegisterHotkey 注册全局热键 func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { - hs.mu.Lock() - defer hs.mu.Unlock() - - // 验证热键组合 if !hs.isValidHotkey(hotkey) { - return &HotkeyError{Operation: "validate_hotkey", Err: fmt.Errorf("invalid hotkey combination")} + return &HotkeyError{"validate", fmt.Errorf("invalid hotkey combination")} } - hs.logger.Info("Hotkey: Registering global hotkey on macOS", - "ctrl", hotkey.Ctrl, - "shift", hotkey.Shift, - "alt", hotkey.Alt, - "win", hotkey.Win, - "key", hotkey.Key) + hs.mu.Lock() + defer hs.mu.Unlock() // 转换键码和修饰符 keyCode := hs.keyToMacKeyCode(hotkey.Key) if keyCode == 0 { - return &HotkeyError{Operation: "convert_key", Err: fmt.Errorf("unsupported key: %s", hotkey.Key)} + return &HotkeyError{"convert_key", fmt.Errorf("unsupported key: %s", hotkey.Key)} } modifiers := hs.buildMacModifiers(hotkey) @@ -170,13 +160,12 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { // 调用C函数注册热键 result := int(C.registerGlobalHotkey(C.int(keyCode), C.int(modifiers))) if result == 0 { - return &HotkeyError{Operation: "register_hotkey", Err: fmt.Errorf("failed to register hotkey")} + return &HotkeyError{"register", fmt.Errorf("failed to register hotkey")} } hs.currentHotkey = hotkey - hs.isRegistered = true + hs.isRegistered.Store(true) - hs.logger.Info("Hotkey: Successfully registered global hotkey on macOS") return nil } @@ -185,67 +174,28 @@ func (hs *HotkeyService) UnregisterHotkey() error { hs.mu.Lock() defer hs.mu.Unlock() - if !hs.isRegistered { - hs.logger.Debug("Hotkey: No hotkey registered, skipping unregister") + if !hs.isRegistered.Load() { return nil } - hs.logger.Info("Hotkey: Unregistering global hotkey on macOS") - // 调用C函数取消注册热键 result := int(C.unregisterGlobalHotkey()) if result == 0 { - return &HotkeyError{Operation: "unregister_hotkey", Err: fmt.Errorf("failed to unregister hotkey")} + return &HotkeyError{"unregister", fmt.Errorf("failed to unregister hotkey")} } hs.currentHotkey = nil - hs.isRegistered = false + hs.isRegistered.Store(false) - hs.logger.Info("Hotkey: Successfully unregistered global hotkey on macOS") return nil } // UpdateHotkey 更新热键配置 func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error { - hs.logger.Info("Hotkey: === UpdateHotkey called (macOS) ===", - "enable", enable, - "ctrl", hotkey.Ctrl, - "shift", hotkey.Shift, - "alt", hotkey.Alt, - "win", hotkey.Win, - "key", hotkey.Key) - if enable { - // 启用热键:直接注册新热键(RegisterHotkey 会处理取消旧热键) - err := hs.RegisterHotkey(hotkey) - if err != nil { - hs.logger.Error("Hotkey: Failed to register new hotkey", "error", err) - return err - } - hs.logger.Info("Hotkey: Successfully updated and registered new hotkey on macOS") - return nil - } else { - // 禁用热键:取消注册 - err := hs.UnregisterHotkey() - if err != nil { - hs.logger.Error("Hotkey: Failed to unregister hotkey", "error", err) - return err - } - hs.logger.Info("Hotkey: Successfully disabled hotkey on macOS") - return nil + return hs.RegisterHotkey(hotkey) } -} - -// ToggleWindow 切换窗口显示/隐藏 -func (hs *HotkeyService) ToggleWindow() { - if hs.app == nil { - hs.logger.Warning("Hotkey: App is nil, cannot toggle") - return - } - - // 发送事件到前端,让前端处理窗口切换 - hs.app.EmitEvent("hotkey:toggle-window", nil) - hs.logger.Debug("Hotkey: Emitted toggle window event (macOS)") + return hs.UnregisterHotkey() } // keyToMacKeyCode 将键名转换为macOS虚拟键码 @@ -330,31 +280,18 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { // IsRegistered 检查是否已注册热键 func (hs *HotkeyService) IsRegistered() bool { - hs.mu.RLock() - defer hs.mu.RUnlock() - return hs.isRegistered + return hs.isRegistered.Load() } // ServiceShutdown 关闭热键服务 func (hs *HotkeyService) ServiceShutdown() error { - hs.mu.Lock() - defer hs.mu.Unlock() - - if hs.isRegistered { - err := hs.UnregisterHotkey() - if err != nil { - hs.logger.Error("Hotkey: Failed to unregister hotkey on shutdown", "error", err) - } - } - - hs.logger.Info("Hotkey: macOS service shutdown completed") - return nil + return hs.UnregisterHotkey() } //export hotkeyTriggered func hotkeyTriggered() { // 通过全局实例调用ToggleWindow - if globalHotkeyService != nil { + if globalHotkeyService != nil && globalHotkeyService.app != nil { globalHotkeyService.ToggleWindow() } } diff --git a/internal/services/hotkey_service_linux.go b/internal/services/hotkey_service_linux.go index c9fdfa7..b9301eb 100644 --- a/internal/services/hotkey_service_linux.go +++ b/internal/services/hotkey_service_linux.go @@ -127,8 +127,10 @@ int isHotkeyRegistered() { import "C" import ( + "context" "fmt" "sync" + "sync/atomic" "time" "unsafe" "voidraft/internal/models" @@ -142,12 +144,14 @@ type HotkeyService struct { logger *log.LoggerService configService *ConfigService app *application.App + mu sync.RWMutex - isRegistered bool currentHotkey *models.HotkeyCombo - stopChan chan struct{} - wg sync.WaitGroup - running bool + isRegistered atomic.Bool + + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup } // HotkeyError 热键错误 @@ -158,25 +162,25 @@ type HotkeyError struct { // Error 实现error接口 func (e *HotkeyError) Error() string { - return fmt.Sprintf("hotkey error during %s: %v", e.Operation, e.Err) + return fmt.Sprintf("hotkey %s: %v", e.Operation, e.Err) } -// Unwrap 获取原始错误 func (e *HotkeyError) Unwrap() error { return e.Err } -// NewHotkeyService 创建新的热键服务实例 +// NewHotkeyService 创建热键服务实例 func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService { if logger == nil { logger = log.New() } + ctx, cancel := context.WithCancel(context.Background()) return &HotkeyService{ logger: logger, configService: configService, - isRegistered: false, - running: false, + ctx: ctx, + cancel: cancel, } } @@ -184,91 +188,73 @@ func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) * func (hs *HotkeyService) Initialize(app *application.App) error { hs.app = app - // 初始化X11显示 if int(C.initX11Display()) == 0 { - return &HotkeyError{Operation: "init_x11", Err: fmt.Errorf("failed to initialize X11 display")} + return &HotkeyError{"init_x11", fmt.Errorf("failed to initialize X11 display")} } - // 加载并应用当前配置 config, err := hs.configService.GetConfig() if err != nil { - return &HotkeyError{Operation: "load_config", Err: err} + return &HotkeyError{"load_config", err} } if config.General.EnableGlobalHotkey { - err = hs.RegisterHotkey(&config.General.GlobalHotkey) - if err != nil { - hs.logger.Error("Hotkey: Failed to register hotkey on startup", "error", err) + if err := hs.RegisterHotkey(&config.General.GlobalHotkey); err != nil { + hs.logger.Error("failed to register startup hotkey", "error", err) } } - hs.logger.Info("Hotkey: Linux service initialized") return nil } // RegisterHotkey 注册全局热键 func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { + if !hs.isValidHotkey(hotkey) { + return &HotkeyError{"validate", fmt.Errorf("invalid hotkey combination")} + } + hs.mu.Lock() defer hs.mu.Unlock() - // 先取消注册现有热键 - if hs.isRegistered { - hs.logger.Info("Hotkey: Unregistering existing hotkey before registering new one") - err := hs.unregisterHotkeyInternal() - if err != nil { - return err - } + // 取消现有热键 + if hs.isRegistered.Load() { + hs.unregisterInternal() } - // 验证热键组合 - if !hs.isValidHotkey(hotkey) { - return &HotkeyError{Operation: "validate_hotkey", Err: fmt.Errorf("invalid hotkey combination")} - } - - hs.logger.Info("Hotkey: Registering global hotkey on Linux", - "ctrl", hotkey.Ctrl, - "shift", hotkey.Shift, - "alt", hotkey.Alt, - "win", hotkey.Win, - "key", hotkey.Key) - - // 转换键码和修饰符 keyCode := hs.keyToX11KeyCode(hotkey.Key) if keyCode == 0 { - return &HotkeyError{Operation: "convert_key", Err: fmt.Errorf("unsupported key: %s", hotkey.Key)} + return &HotkeyError{"convert_key", fmt.Errorf("unsupported key: %s", hotkey.Key)} } modifiers := hs.buildX11Modifiers(hotkey) - // 调用C函数注册热键 result := int(C.registerGlobalHotkey(C.int(keyCode), C.uint(modifiers))) if result == 0 { - return &HotkeyError{Operation: "register_hotkey", Err: fmt.Errorf("failed to register hotkey")} + return &HotkeyError{"register", fmt.Errorf("failed to register hotkey")} } - // 创建ready channel等待goroutine启动完成 - readyChan := make(chan struct{}) - - // 确保 stopChan 是新的 - hs.stopChan = make(chan struct{}) + // 启动监听器 + ctx, cancel := context.WithCancel(hs.ctx) hs.wg.Add(1) - go hs.hotkeyListener(hotkey, readyChan) - // 等待监听器启动完成,设置超时避免无限等待 + ready := make(chan error, 1) + go hs.hotkeyListener(ctx, ready) + + // 等待启动完成 select { - case <-readyChan: - // 监听器启动完成 - case <-time.After(1 * time.Second): - // 超时处理 - hs.logger.Warning("Hotkey: Timeout waiting for listener to start") - return &HotkeyError{Operation: "start_listener", Err: fmt.Errorf("timeout waiting for hotkey listener to start")} + case err := <-ready: + if err != nil { + cancel() + return &HotkeyError{"start_listener", err} + } + case <-time.After(time.Second): + cancel() + return &HotkeyError{"start_listener", fmt.Errorf("timeout")} } hs.currentHotkey = hotkey - hs.isRegistered = true - hs.running = true + hs.isRegistered.Store(true) + hs.cancel = cancel - hs.logger.Info("Hotkey: Successfully registered global hotkey on Linux") return nil } @@ -276,130 +262,70 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { func (hs *HotkeyService) UnregisterHotkey() error { hs.mu.Lock() defer hs.mu.Unlock() - - return hs.unregisterHotkeyInternal() + return hs.unregisterInternal() } -// unregisterHotkeyInternal 内部取消注册方法(无锁) -func (hs *HotkeyService) unregisterHotkeyInternal() error { - if !hs.isRegistered { - hs.logger.Debug("Hotkey: No hotkey registered, skipping unregister") +// unregisterInternal 内部取消注册(无锁) +func (hs *HotkeyService) unregisterInternal() error { + if !hs.isRegistered.Load() { return nil } - hs.logger.Info("Hotkey: Unregistering global hotkey on Linux") - - // 停止监听 - if hs.stopChan != nil { - close(hs.stopChan) - hs.logger.Debug("Hotkey: Waiting for listener goroutine to stop") + if hs.cancel != nil { + hs.cancel() hs.wg.Wait() - hs.logger.Debug("Hotkey: Listener goroutine stopped") } - // 调用C函数取消注册热键 result := int(C.unregisterGlobalHotkey()) if result == 0 { - return &HotkeyError{Operation: "unregister_hotkey", Err: fmt.Errorf("failed to unregister hotkey")} + return &HotkeyError{"unregister", fmt.Errorf("failed to unregister hotkey")} } - // 重置状态 hs.currentHotkey = nil - hs.isRegistered = false - hs.running = false - hs.stopChan = nil - - hs.logger.Info("Hotkey: Successfully unregistered global hotkey on Linux") + hs.isRegistered.Store(false) return nil } // UpdateHotkey 更新热键配置 func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error { - hs.logger.Info("Hotkey: === UpdateHotkey called (Linux) ===", - "enable", enable, - "ctrl", hotkey.Ctrl, - "shift", hotkey.Shift, - "alt", hotkey.Alt, - "win", hotkey.Win, - "key", hotkey.Key) - if enable { - // 启用热键:直接注册新热键(RegisterHotkey 会处理取消旧热键) - err := hs.RegisterHotkey(hotkey) - if err != nil { - hs.logger.Error("Hotkey: Failed to register new hotkey", "error", err) - return err - } - hs.logger.Info("Hotkey: Successfully updated and registered new hotkey on Linux") - return nil - } else { - // 禁用热键:取消注册 - err := hs.UnregisterHotkey() - if err != nil { - hs.logger.Error("Hotkey: Failed to unregister hotkey", "error", err) - return err - } - hs.logger.Info("Hotkey: Successfully disabled hotkey on Linux") - return nil + return hs.RegisterHotkey(hotkey) } + return hs.UnregisterHotkey() } -// ToggleWindow 切换窗口显示/隐藏 -func (hs *HotkeyService) ToggleWindow() { - if hs.app == nil { - hs.logger.Warning("Hotkey: App is nil, cannot toggle") - return - } - - // 发送事件到前端,让前端处理窗口切换 - hs.app.EmitEvent("hotkey:toggle-window", nil) - hs.logger.Debug("Hotkey: Emitted toggle window event (Linux)") -} - -// hotkeyListener 热键监听器goroutine -func (hs *HotkeyService) hotkeyListener(hotkey *models.HotkeyCombo, readyChan chan struct{}) { +// hotkeyListener 热键监听器 +func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error) { defer hs.wg.Done() - hs.logger.Debug("Hotkey: Starting Linux X11 hotkey listener") - - // 检查间隔(100ms) ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() - // 标记是否已经发送ready信号,确保只发送一次 - readySent := false + ready <- nil // 标记准备就绪 for { select { - case <-hs.stopChan: - hs.logger.Debug("Hotkey: Stopping Linux X11 hotkey listener") - if !readySent { - close(readyChan) - } + case <-ctx.Done(): return case <-ticker.C: - // 第一次循环时发送ready信号,表示监听器已经准备就绪 - if !readySent { - close(readyChan) - readySent = true - hs.logger.Debug("Hotkey: Listener ready signal sent") - } - - // 检查热键事件 if int(C.checkHotkeyEvent()) == 1 { - hs.logger.Debug("Hotkey: Global hotkey triggered via Linux X11") - hs.ToggleWindow() + hs.toggleWindow() } } } } -// keyToX11KeyCode 将键名转换为X11键码 +// toggleWindow 切换窗口 +func (hs *HotkeyService) toggleWindow() { + if hs.app != nil { + hs.app.EmitEvent("hotkey:toggle-window", nil) + } +} + +// keyToX11KeyCode 键名转X11键码 func (hs *HotkeyService) keyToX11KeyCode(key string) int { - // 将Go字符串转换为C字符串 cKey := C.CString(key) defer C.free(unsafe.Pointer(cKey)) - return int(C.getX11Keycode(cKey)) } @@ -423,27 +349,19 @@ func (hs *HotkeyService) buildX11Modifiers(hotkey *models.HotkeyCombo) uint { return modifiers } -// isValidHotkey 验证热键组合是否有效 +// isValidHotkey 验证热键组合 func (hs *HotkeyService) isValidHotkey(hotkey *models.HotkeyCombo) bool { - if hotkey == nil { + if hotkey == nil || hotkey.Key == "" { return false } - - // 必须有主键 - if hotkey.Key == "" { - return false - } - - // 必须至少有一个修饰键 + // 至少需要一个修饰键 if !hotkey.Ctrl && !hotkey.Shift && !hotkey.Alt && !hotkey.Win { return false } - - // 验证主键是否在有效范围内 return hs.keyToX11KeyCode(hotkey.Key) != 0 } -// GetCurrentHotkey 获取当前注册的热键 +// GetCurrentHotkey 获取当前热键 func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { hs.mu.RLock() defer hs.mu.RUnlock() @@ -452,7 +370,6 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { return nil } - // 返回副本避免并发问题 return &models.HotkeyCombo{ Ctrl: hs.currentHotkey.Ctrl, Shift: hs.currentHotkey.Shift, @@ -462,28 +379,15 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { } } -// IsRegistered 检查是否已注册热键 +// IsRegistered 检查是否已注册 func (hs *HotkeyService) IsRegistered() bool { - hs.mu.RLock() - defer hs.mu.RUnlock() - return hs.isRegistered + return hs.isRegistered.Load() } -// ServiceShutdown 关闭热键服务 +// ServiceShutdown 关闭服务 func (hs *HotkeyService) ServiceShutdown() error { - hs.mu.Lock() - defer hs.mu.Unlock() - - if hs.isRegistered { - err := hs.unregisterHotkeyInternal() - if err != nil { - hs.logger.Error("Hotkey: Failed to unregister hotkey on shutdown", "error", err) - } - } - - // 关闭X11显示 + hs.cancel() + hs.wg.Wait() C.closeX11Display() - - hs.logger.Info("Hotkey: Linux service shutdown completed") return nil } diff --git a/internal/services/hotkey_service_stub.go b/internal/services/hotkey_service_stub.go index d57bb37..6a1683d 100644 --- a/internal/services/hotkey_service_stub.go +++ b/internal/services/hotkey_service_stub.go @@ -10,7 +10,7 @@ import ( "github.com/wailsapp/wails/v3/pkg/services/log" ) -// HotkeyService 全局热键服务 +// HotkeyService 存根热键服务 type HotkeyService struct { logger *log.LoggerService configService *ConfigService @@ -24,7 +24,7 @@ type HotkeyError struct { // Error 实现error接口 func (e *HotkeyError) Error() string { - return fmt.Sprintf("hotkey error during %s: %v", e.Operation, e.Err) + return fmt.Sprintf("hotkey %s: %v", e.Operation, e.Err) } // Unwrap 获取原始错误 @@ -32,7 +32,7 @@ func (e *HotkeyError) Unwrap() error { return e.Err } -// NewHotkeyService 创建新的热键服务实例 +// NewHotkeyService 创建热键服务实例 func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService { if logger == nil { logger = log.New() @@ -46,40 +46,39 @@ func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) * // Initialize 初始化热键服务 func (hs *HotkeyService) Initialize(app *application.App) error { - hs.logger.Warning("Hotkey: Global hotkey is not supported on this platform") + hs.logger.Warning("Global hotkey is not supported on this platform") return nil } // RegisterHotkey 注册全局热键 func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { - return &HotkeyError{Operation: "register", Err: fmt.Errorf("not supported on this platform")} + return &HotkeyError{"register", fmt.Errorf("not supported on this platform")} } // UnregisterHotkey 取消注册全局热键 func (hs *HotkeyService) UnregisterHotkey() error { - return &HotkeyError{Operation: "unregister", Err: fmt.Errorf("not supported on this platform")} + return &HotkeyError{"unregister", fmt.Errorf("not supported on this platform")} } // UpdateHotkey 更新热键配置 func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error { if enable { return hs.RegisterHotkey(hotkey) - } else { - return hs.UnregisterHotkey() } + return hs.UnregisterHotkey() } -// GetCurrentHotkey 获取当前注册的热键 +// GetCurrentHotkey 获取当前热键 func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { return nil } -// IsRegistered 检查是否已注册热键 +// IsRegistered 检查是否已注册 func (hs *HotkeyService) IsRegistered() bool { return false } -// ServiceShutdown 关闭热键服务 +// ServiceShutdown 关闭服务 func (hs *HotkeyService) ServiceShutdown() error { return nil } diff --git a/internal/services/keybinding_service.go b/internal/services/keybinding_service.go index ddb61d7..af3da9d 100644 --- a/internal/services/keybinding_service.go +++ b/internal/services/keybinding_service.go @@ -1,14 +1,12 @@ package services import ( + "context" "encoding/json" "errors" "fmt" "os" - "path/filepath" - "strings" "sync" - "time" "voidraft/internal/models" "github.com/fsnotify/fsnotify" @@ -18,96 +16,90 @@ import ( // KeyBindingService 快捷键管理服务 type KeyBindingService struct { - viper *viper.Viper // Viper 实例 - logger *log.LoggerService // 日志服务 - mu sync.RWMutex // 读写锁 + viper *viper.Viper + logger *log.LoggerService + pathManager *PathManager + + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + initOnce sync.Once } // KeyBindingError 快捷键错误 type KeyBindingError struct { - Operation string // 操作名称 - Command string // 快捷键Command - Err error // 原始错误 + Operation string + Command string + Err error } -// Error 实现error接口 func (e *KeyBindingError) Error() string { if e.Command != "" { - return fmt.Sprintf("keybinding error during %s for command %s: %v", e.Operation, e.Command, e.Err) + return fmt.Sprintf("keybinding %s for %s: %v", e.Operation, e.Command, e.Err) } - return fmt.Sprintf("keybinding error during %s: %v", e.Operation, e.Err) + return fmt.Sprintf("keybinding %s: %v", e.Operation, e.Err) } -// Unwrap 获取原始错误 func (e *KeyBindingError) Unwrap() error { return e.Err } -// Is 实现错误匹配 func (e *KeyBindingError) Is(target error) bool { var keyBindingError *KeyBindingError - ok := errors.As(target, &keyBindingError) - return ok + return errors.As(target, &keyBindingError) } -// NewKeyBindingService 创建新的快捷键服务实例 -func NewKeyBindingService(logger *log.LoggerService) *KeyBindingService { - // 设置日志服务 +// NewKeyBindingService 创建快捷键服务实例 +func NewKeyBindingService(logger *log.LoggerService, pathManager *PathManager) *KeyBindingService { if logger == nil { logger = log.New() } - - // 获取当前工作目录 - currentDir, err := os.Getwd() - if err != nil { - currentDir = "." + if pathManager == nil { + pathManager = NewPathManager() } - // 固定配置路径和文件名 - configPath := filepath.Join(currentDir, "config") - configName := "keybindings" + ctx, cancel := context.WithCancel(context.Background()) - // 创建 Viper 实例 + // 创建并配置 Viper v := viper.New() - - // 配置 Viper - v.SetConfigName(configName) + v.SetConfigName(pathManager.GetKeybindsName()) v.SetConfigType("json") - v.AddConfigPath(configPath) - - // 设置环境变量前缀 + v.AddConfigPath(pathManager.GetConfigDir()) v.SetEnvPrefix("VOIDRAFT_KEYBINDING") v.AutomaticEnv() - // 设置默认值 - setKeyBindingDefaults(v) - - // 构造快捷键服务实例 service := &KeyBindingService{ - viper: v, - logger: logger, + viper: v, + logger: logger, + pathManager: pathManager, + ctx: ctx, + cancel: cancel, } - // 初始化配置 - if err := service.initConfig(); err != nil { - service.logger.Error("KeyBinding: Failed to initialize keybinding config", "error", err) - } - - // 启动配置文件监听 - service.startWatching() + // 异步初始化 + go service.initialize() return service } -// setKeyBindingDefaults 设置默认快捷键配置值 -func setKeyBindingDefaults(v *viper.Viper) { +// initialize 初始化配置 +func (kbs *KeyBindingService) initialize() { + kbs.initOnce.Do(func() { + kbs.setDefaults() + + if err := kbs.initConfig(); err != nil { + kbs.logger.Error("failed to initialize keybinding config", "error", err) + } + + kbs.startWatching() + }) +} + +// setDefaults 设置默认值 +func (kbs *KeyBindingService) setDefaults() { defaultConfig := models.NewDefaultKeyBindingConfig() - - // 快捷键列表默认值 - v.SetDefault("keyBindings", defaultConfig.KeyBindings) - - // 元数据默认值 - v.SetDefault("metadata.lastUpdated", defaultConfig.Metadata.LastUpdated) + kbs.viper.SetDefault("keyBindings", defaultConfig.KeyBindings) + kbs.viper.SetDefault("metadata.lastUpdated", defaultConfig.Metadata.LastUpdated) } // initConfig 初始化配置 @@ -115,68 +107,41 @@ func (kbs *KeyBindingService) initConfig() error { kbs.mu.Lock() defer kbs.mu.Unlock() - // 尝试读取配置文件 if err := kbs.viper.ReadInConfig(); err != nil { var configFileNotFoundError viper.ConfigFileNotFoundError if errors.As(err, &configFileNotFoundError) { - // 配置文件不存在,创建默认配置文件 - kbs.logger.Info("KeyBinding: Config file not found, creating default keybinding config") return kbs.createDefaultConfig() } - // 配置文件存在但读取失败 - return &KeyBindingError{Operation: "read_keybinding_config", Err: err} + return &KeyBindingError{"read_config", "", err} } - - kbs.logger.Info("KeyBinding: Successfully loaded keybinding config file", "file", kbs.viper.ConfigFileUsed()) return nil } // createDefaultConfig 创建默认配置文件 func (kbs *KeyBindingService) createDefaultConfig() error { - // 获取配置目录路径 - currentDir, err := os.Getwd() - if err != nil { - currentDir = "." - } - configDir := filepath.Join(currentDir, "config") - configPath := filepath.Join(configDir, "keybindings.json") - - // 确保配置目录存在 - if err := os.MkdirAll(configDir, 0755); err != nil { - return &KeyBindingError{Operation: "create_keybinding_config_dir", Err: err} + if err := kbs.pathManager.EnsureConfigDir(); err != nil { + return &KeyBindingError{"create_config_dir", "", err} } - // 获取默认配置 defaultConfig := models.NewDefaultKeyBindingConfig() configBytes, err := json.MarshalIndent(defaultConfig, "", " ") if err != nil { - return &KeyBindingError{Operation: "marshal_default_keybinding_config", Err: err} + return &KeyBindingError{"marshal_config", "", err} } - // 写入配置文件 - if err := os.WriteFile(configPath, configBytes, 0644); err != nil { - return &KeyBindingError{Operation: "write_default_keybinding_config", Err: err} + if err := os.WriteFile(kbs.pathManager.GetKeybindsPath(), configBytes, 0644); err != nil { + return &KeyBindingError{"write_config", "", err} } - // 重新读取配置文件到viper - if err := kbs.viper.ReadInConfig(); err != nil { - return &KeyBindingError{Operation: "read_created_keybinding_config", Err: err} - } - - kbs.logger.Info("KeyBinding: Created default keybinding config file", "path", configPath) - return nil + return kbs.viper.ReadInConfig() } // startWatching 启动配置文件监听 func (kbs *KeyBindingService) startWatching() { - // 设置配置变化回调 kbs.viper.OnConfigChange(func(e fsnotify.Event) { - kbs.logger.Info("KeyBinding: Config file changed", "file", e.Name, "operation", e.Op.String()) - }) - // 启动配置文件监听 + }) kbs.viper.WatchConfig() - kbs.logger.Info("KeyBinding: Started watching keybinding config file for changes") } // GetKeyBindingConfig 获取完整快捷键配置 @@ -186,392 +151,22 @@ func (kbs *KeyBindingService) GetKeyBindingConfig() (*models.KeyBindingConfig, e var config models.KeyBindingConfig if err := kbs.viper.Unmarshal(&config); err != nil { - return nil, &KeyBindingError{Operation: "unmarshal_keybinding_config", Err: err} + return nil, &KeyBindingError{"unmarshal_config", "", err} } - return &config, nil } // GetAllKeyBindings 获取所有快捷键配置 func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) { - kbs.mu.RLock() - defer kbs.mu.RUnlock() - config, err := kbs.GetKeyBindingConfig() if err != nil { - return nil, &KeyBindingError{Operation: "get_all_keybindings", Err: err} + return nil, err } - return config.KeyBindings, nil } -// GetKeyBindingsByCategory 根据分类获取快捷键 -func (kbs *KeyBindingService) GetKeyBindingsByCategory(category models.KeyBindingCategory) ([]models.KeyBinding, error) { - kbs.mu.RLock() - defer kbs.mu.RUnlock() - - allKeyBindings, err := kbs.GetAllKeyBindings() - if err != nil { - return nil, err - } - - var result []models.KeyBinding - for _, kb := range allKeyBindings { - if kb.Category == category { - result = append(result, kb) - } - } - - return result, nil -} - -// GetKeyBindingByCommand 根据命令获取快捷键 -func (kbs *KeyBindingService) GetKeyBindingByCommand(command models.KeyBindingCommand) (*models.KeyBinding, error) { - kbs.mu.RLock() - defer kbs.mu.RUnlock() - - allKeyBindings, err := kbs.GetAllKeyBindings() - if err != nil { - return nil, err - } - - for _, kb := range allKeyBindings { - if kb.Command == command && kb.Enabled { - return &kb, nil - } - } - - return nil, &KeyBindingError{ - Operation: "get_keybinding_by_command", - Err: fmt.Errorf("keybinding for command %s not found", command), - } -} - -// UpdateKeyBinding 更新快捷键 -func (kbs *KeyBindingService) UpdateKeyBinding(command models.KeyBindingCommand, newKey string) error { - kbs.mu.Lock() - defer kbs.mu.Unlock() - - // 验证新的快捷键格式 - if err := kbs.validateKeyFormat(newKey); err != nil { - return &KeyBindingError{ - Operation: "update_keybinding", - Command: string(command), - Err: fmt.Errorf("invalid key format: %v", err), - } - } - - // 检查快捷键冲突 - if err := kbs.checkKeyConflict(command, newKey); err != nil { - return &KeyBindingError{ - Operation: "update_keybinding", - Command: string(command), - Err: fmt.Errorf("key conflict: %v", err), - } - } - - // 获取当前配置 - config, err := kbs.GetKeyBindingConfig() - if err != nil { - return &KeyBindingError{Operation: "update_keybinding", Command: string(command), Err: err} - } - - // 查找并更新快捷键 - found := false - for i, kb := range config.KeyBindings { - if kb.Command == command { - config.KeyBindings[i].Key = newKey - config.KeyBindings[i].IsDefault = false // 标记为非默认 - found = true - break - } - } - - if !found { - return &KeyBindingError{ - Operation: "update_keybinding", - Command: string(command), - Err: errors.New("keybinding not found"), - } - } - - // 更新时间戳 - config.Metadata.LastUpdated = time.Now().Format(time.RFC3339) - - // 保存配置 - if err := kbs.saveConfig(config); err != nil { - return &KeyBindingError{Operation: "update_keybinding", Command: string(command), Err: err} - } - - kbs.logger.Info("KeyBinding: Updated keybinding", "command", command, "newKey", newKey) - return nil -} - -// EnableKeyBinding 启用快捷键 -func (kbs *KeyBindingService) EnableKeyBinding(command models.KeyBindingCommand) error { - return kbs.setKeyBindingEnabled(command, true) -} - -// DisableKeyBinding 禁用快捷键 -func (kbs *KeyBindingService) DisableKeyBinding(command models.KeyBindingCommand) error { - return kbs.setKeyBindingEnabled(command, false) -} - -// setKeyBindingEnabled 设置快捷键启用状态 -func (kbs *KeyBindingService) setKeyBindingEnabled(command models.KeyBindingCommand, enabled bool) error { - kbs.mu.Lock() - defer kbs.mu.Unlock() - - // 获取当前配置 - config, err := kbs.GetKeyBindingConfig() - if err != nil { - return &KeyBindingError{Operation: "set_keybinding_enabled", Command: string(command), Err: err} - } - - // 查找并更新快捷键 - found := false - for i, kb := range config.KeyBindings { - if kb.Command == command { - config.KeyBindings[i].Enabled = enabled - found = true - break - } - } - - if !found { - return &KeyBindingError{ - Operation: "set_keybinding_enabled", - Command: string(command), - Err: errors.New("keybinding not found"), - } - } - - // 更新时间戳 - config.Metadata.LastUpdated = time.Now().Format(time.RFC3339) - - // 保存配置 - if err := kbs.saveConfig(config); err != nil { - return &KeyBindingError{Operation: "set_keybinding_enabled", Command: string(command), Err: err} - } - - status := "enabled" - if !enabled { - status = "disabled" - } - kbs.logger.Info("KeyBinding: "+status+" keybinding", "command", command) - return nil -} - -// ResetKeyBinding 重置快捷键到默认值 -func (kbs *KeyBindingService) ResetKeyBinding(command models.KeyBindingCommand) error { - kbs.mu.Lock() - defer kbs.mu.Unlock() - - // 获取默认配置 - defaultKeyBindings := models.NewDefaultKeyBindings() - var defaultKeyBinding *models.KeyBinding - for _, kb := range defaultKeyBindings { - if kb.Command == command { - defaultKeyBinding = &kb - break - } - } - - if defaultKeyBinding == nil { - return &KeyBindingError{ - Operation: "reset_keybinding", - Command: string(command), - Err: errors.New("default keybinding not found"), - } - } - - // 获取当前配置 - config, err := kbs.GetKeyBindingConfig() - if err != nil { - return &KeyBindingError{Operation: "reset_keybinding", Command: string(command), Err: err} - } - - // 查找并重置快捷键 - found := false - for i, kb := range config.KeyBindings { - if kb.Command == command { - config.KeyBindings[i].Key = defaultKeyBinding.Key - config.KeyBindings[i].Enabled = defaultKeyBinding.Enabled - config.KeyBindings[i].IsDefault = true - found = true - break - } - } - - if !found { - return &KeyBindingError{ - Operation: "reset_keybinding", - Command: string(command), - Err: errors.New("keybinding not found"), - } - } - - // 更新时间戳 - config.Metadata.LastUpdated = time.Now().Format(time.RFC3339) - - // 保存配置 - if err := kbs.saveConfig(config); err != nil { - return &KeyBindingError{Operation: "reset_keybinding", Command: string(command), Err: err} - } - - kbs.logger.Info("KeyBinding: Reset keybinding to default", "command", command, "key", defaultKeyBinding.Key) - return nil -} - -// ResetAllKeyBindings 重置所有快捷键到默认值 -func (kbs *KeyBindingService) ResetAllKeyBindings() error { - kbs.mu.Lock() - defer kbs.mu.Unlock() - - // 获取默认配置 - defaultConfig := models.NewDefaultKeyBindingConfig() - - // 保存配置 - if err := kbs.saveConfig(defaultConfig); err != nil { - return &KeyBindingError{Operation: "reset_all_keybindings", Err: err} - } - - kbs.logger.Info("KeyBinding: Reset all keybindings to default") - return nil -} - -// saveConfig 保存配置到文件 -func (kbs *KeyBindingService) saveConfig(config *models.KeyBindingConfig) error { - // 设置快捷键列表到viper - kbs.viper.Set("keyBindings", config.KeyBindings) - kbs.viper.Set("metadata.lastUpdated", config.Metadata.LastUpdated) - - // 写入配置文件 - if err := kbs.viper.WriteConfig(); err != nil { - return fmt.Errorf("failed to write keybinding config: %v", err) - } - - return nil -} - -// validateKeyFormat 验证快捷键格式 -func (kbs *KeyBindingService) validateKeyFormat(key string) error { - if key == "" { - return errors.New("key cannot be empty") - } - - // 基本格式验证 - // 支持的修饰符: Mod, Ctrl, Shift, Alt, Win - // 支持的组合: Mod-f, Ctrl-Shift-p, Alt-ArrowUp 等 - validModifiers := []string{"Mod", "Ctrl", "Shift", "Alt", "Win"} - parts := strings.Split(key, "-") - - if len(parts) == 0 { - return errors.New("invalid key format") - } - - // 检查修饰符 - for i := 0; i < len(parts)-1; i++ { - modifier := parts[i] - valid := false - for _, validMod := range validModifiers { - if modifier == validMod { - valid = true - break - } - } - if !valid { - return fmt.Errorf("invalid modifier: %s", modifier) - } - } - - // 最后一部分应该是主键 - mainKey := parts[len(parts)-1] - if mainKey == "" { - return errors.New("main key cannot be empty") - } - - return nil -} - -// checkKeyConflict 检查快捷键冲突 -func (kbs *KeyBindingService) checkKeyConflict(excludeCommand models.KeyBindingCommand, key string) error { - allKeyBindings, err := kbs.GetAllKeyBindings() - if err != nil { - return err - } - - for _, kb := range allKeyBindings { - if kb.Command != excludeCommand && kb.Key == key && kb.Enabled { - return fmt.Errorf("key %s is already used by %s", key, kb.Command) - } - } - - return nil -} - -// GetKeyBindingCategories 获取所有快捷键分类 -func (kbs *KeyBindingService) GetKeyBindingCategories() []models.KeyBindingCategory { - return []models.KeyBindingCategory{ - models.CategorySearch, - models.CategoryEdit, - models.CategoryCodeBlock, - models.CategoryHistory, - models.CategoryFold, - } -} - -// ExportKeyBindings 导出快捷键配置 -func (kbs *KeyBindingService) ExportKeyBindings() ([]models.KeyBinding, error) { - kbs.mu.RLock() - defer kbs.mu.RUnlock() - - return kbs.GetAllKeyBindings() -} - -// ImportKeyBindings 导入快捷键配置 -func (kbs *KeyBindingService) ImportKeyBindings(keyBindings []models.KeyBinding) error { - kbs.mu.Lock() - defer kbs.mu.Unlock() - - // 验证导入的快捷键 - for _, kb := range keyBindings { - if err := kbs.validateKeyFormat(kb.Key); err != nil { - return &KeyBindingError{ - Operation: "import_keybindings", - Command: string(kb.Command), - Err: fmt.Errorf("invalid key format for %s: %v", kb.Command, err), - } - } - } - - // 检查重复的快捷键 - keyMap := make(map[string]models.KeyBindingCommand) - for _, kb := range keyBindings { - if kb.Enabled { - if existingCommand, exists := keyMap[kb.Key]; exists { - return &KeyBindingError{ - Operation: "import_keybindings", - Err: fmt.Errorf("duplicate key %s found in %s and %s", kb.Key, existingCommand, kb.Command), - } - } - keyMap[kb.Key] = kb.Command - } - } - - // 创建新的配置 - config := &models.KeyBindingConfig{ - KeyBindings: keyBindings, - Metadata: models.KeyBindingMetadata{ - LastUpdated: time.Now().Format(time.RFC3339), - }, - } - - // 保存配置 - if err := kbs.saveConfig(config); err != nil { - return &KeyBindingError{Operation: "import_keybindings", Err: err} - } - - kbs.logger.Info("KeyBinding: Imported keybindings", "count", len(keyBindings)) +// Shutdown 关闭服务 +func (kbs *KeyBindingService) Shutdown() error { + kbs.cancel() return nil } diff --git a/internal/services/migration_service.go b/internal/services/migration_service.go index 97f435b..5d87790 100644 --- a/internal/services/migration_service.go +++ b/internal/services/migration_service.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "time" "github.com/wailsapp/wails/v3/pkg/services/log" @@ -18,25 +19,26 @@ import ( type MigrationStatus string const ( - MigrationStatusMigrating MigrationStatus = "migrating" // 迁移中 - MigrationStatusCompleted MigrationStatus = "completed" // 完成 - MigrationStatusFailed MigrationStatus = "failed" // 失败 + MigrationStatusMigrating MigrationStatus = "migrating" + MigrationStatusCompleted MigrationStatus = "completed" + MigrationStatusFailed MigrationStatus = "failed" ) // MigrationProgress 迁移进度信息 type MigrationProgress struct { - Status MigrationStatus `json:"status"` // 迁移状态 - Progress float64 `json:"progress"` // 进度百分比 (0-100) - Error string `json:"error,omitempty"` // 错误信息 + Status MigrationStatus `json:"status"` + Progress float64 `json:"progress"` + Error string `json:"error,omitempty"` } // MigrationService 迁移服务 type MigrationService struct { - logger *log.LoggerService - mu sync.RWMutex - currentProgress MigrationProgress - cancelFunc context.CancelFunc - ctx context.Context + logger *log.LoggerService + mu sync.RWMutex + progress atomic.Value // stores MigrationProgress + + ctx context.Context + cancel context.CancelFunc } // NewMigrationService 创建迁移服务 @@ -45,27 +47,27 @@ func NewMigrationService(logger *log.LoggerService) *MigrationService { logger = log.New() } - return &MigrationService{ + ms := &MigrationService{ logger: logger, - currentProgress: MigrationProgress{ - Status: MigrationStatusCompleted, // 初始状态为完成 - Progress: 0, - }, } + + // 初始化进度 + ms.progress.Store(MigrationProgress{ + Status: MigrationStatusCompleted, + Progress: 0, + }) + + return ms } // GetProgress 获取当前进度 func (ms *MigrationService) GetProgress() MigrationProgress { - ms.mu.RLock() - defer ms.mu.RUnlock() - return ms.currentProgress + return ms.progress.Load().(MigrationProgress) } // updateProgress 更新进度 func (ms *MigrationService) updateProgress(progress MigrationProgress) { - ms.mu.Lock() - ms.currentProgress = progress - ms.mu.Unlock() + ms.progress.Store(progress) } // MigrateDirectory 迁移目录 @@ -74,71 +76,83 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error { ctx, cancel := context.WithCancel(context.Background()) ms.mu.Lock() ms.ctx = ctx - ms.cancelFunc = cancel + ms.cancel = cancel ms.mu.Unlock() defer func() { ms.mu.Lock() - ms.cancelFunc = nil + ms.cancel = nil ms.ctx = nil ms.mu.Unlock() }() - ms.logger.Info("Migration: Starting directory migration", "from", srcPath, "to", dstPath) - // 初始化进度 - progress := MigrationProgress{ + ms.updateProgress(MigrationProgress{ Status: MigrationStatusMigrating, Progress: 0, - } - ms.updateProgress(progress) + }) + // 预检查 + if err := ms.preCheck(srcPath, dstPath); err != nil { + if err == errNoMigrationNeeded { + ms.updateProgress(MigrationProgress{ + Status: MigrationStatusCompleted, + Progress: 100, + }) + return nil + } + return ms.failWithError(err) + } + + // 执行原子迁移 + if err := ms.atomicMove(ctx, srcPath, dstPath); err != nil { + return ms.failWithError(err) + } + + // 迁移完成 + ms.updateProgress(MigrationProgress{ + Status: MigrationStatusCompleted, + Progress: 100, + }) + + return nil +} + +var errNoMigrationNeeded = fmt.Errorf("no migration needed") + +// preCheck 预检查 +func (ms *MigrationService) preCheck(srcPath, dstPath string) error { // 检查源目录是否存在 if _, err := os.Stat(srcPath); os.IsNotExist(err) { - progress.Status = MigrationStatusCompleted - progress.Progress = 100 - ms.updateProgress(progress) - return nil + return errNoMigrationNeeded } // 如果路径相同,不需要迁移 srcAbs, _ := filepath.Abs(srcPath) dstAbs, _ := filepath.Abs(dstPath) if srcAbs == dstAbs { - progress.Status = MigrationStatusCompleted - progress.Progress = 100 - ms.updateProgress(progress) - return nil + return errNoMigrationNeeded } // 检查目标路径是否是源路径的子目录 if ms.isSubDirectory(srcAbs, dstAbs) { - progress.Status = MigrationStatusFailed - progress.Error = "Target path cannot be a subdirectory of source path" - ms.updateProgress(progress) return fmt.Errorf("target path cannot be a subdirectory of source path") } - // 执行原子迁移 - err := ms.atomicMove(ctx, srcPath, dstPath, &progress) - if err != nil { - progress.Status = MigrationStatusFailed - progress.Error = err.Error() - ms.updateProgress(progress) - return err - } - - // 迁移完成 - progress.Status = MigrationStatusCompleted - progress.Progress = 100 - ms.updateProgress(progress) - - ms.logger.Info("Migration: Directory migration completed", "from", srcPath, "to", dstPath) return nil } +// failWithError 失败并记录错误 +func (ms *MigrationService) failWithError(err error) error { + ms.updateProgress(MigrationProgress{ + Status: MigrationStatusFailed, + Error: err.Error(), + }) + return err +} + // atomicMove 原子移动目录 -func (ms *MigrationService) atomicMove(ctx context.Context, srcPath, dstPath string, progress *MigrationProgress) error { +func (ms *MigrationService) atomicMove(ctx context.Context, srcPath, dstPath string) error { // 检查是否取消 select { case <-ctx.Done(): @@ -147,68 +161,84 @@ func (ms *MigrationService) atomicMove(ctx context.Context, srcPath, dstPath str } // 确保目标目录的父目录存在 - dstParent := filepath.Dir(dstPath) - if err := os.MkdirAll(dstParent, 0755); err != nil { - return fmt.Errorf("Failed to create target parent directory") + if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { + return fmt.Errorf("failed to create target parent directory: %v", err) } - // 检查目标路径情况 - if stat, err := os.Stat(dstPath); err == nil { - if !stat.IsDir() { - return fmt.Errorf("Target path exists but is not a directory") - } - isEmpty, err := ms.isDirectoryEmpty(dstPath) - if err != nil { - return fmt.Errorf("Failed to check target directory") - } - if !isEmpty { - return fmt.Errorf("Target directory is not empty") - } + // 检查目标路径 + if err := ms.checkTargetPath(dstPath); err != nil { + return err } - // 尝试直接重命名(如果在同一分区,这会很快) - progress.Progress = 20 - ms.updateProgress(*progress) + // 尝试直接重命名 + ms.updateProgress(MigrationProgress{ + Status: MigrationStatusMigrating, + Progress: 20, + }) if err := os.Rename(srcPath, dstPath); err == nil { - ms.logger.Info("Migration: Fast rename successful") return nil - } else { - ms.logger.Info("Migration: Fast rename failed, using copy method", "error", err) } // 重命名失败,使用压缩迁移 - progress.Progress = 30 - ms.updateProgress(*progress) + ms.updateProgress(MigrationProgress{ + Status: MigrationStatusMigrating, + Progress: 30, + }) - return ms.atomicCompressMove(ctx, srcPath, dstPath, progress) + return ms.compressMove(ctx, srcPath, dstPath) } -// atomicCompressMove 原子压缩迁移 -func (ms *MigrationService) atomicCompressMove(ctx context.Context, srcPath, dstPath string, progress *MigrationProgress) error { - tempDir := os.TempDir() - tempZipFile := filepath.Join(tempDir, fmt.Sprintf("voidraft_migration_%d.zip", time.Now().UnixNano())) +// checkTargetPath 检查目标路径 +func (ms *MigrationService) checkTargetPath(dstPath string) error { + stat, err := os.Stat(dstPath) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return fmt.Errorf("failed to check target path: %v", err) + } - defer func() { - if err := os.Remove(tempZipFile); err != nil && !os.IsNotExist(err) { - ms.logger.Error("Migration: Failed to clean up temporary zip file", "error", err) - } - }() + if !stat.IsDir() { + return fmt.Errorf("target path exists but is not a directory") + } + + isEmpty, err := ms.isDirectoryEmpty(dstPath) + if err != nil { + return fmt.Errorf("failed to check target directory: %v", err) + } + if !isEmpty { + return fmt.Errorf("target directory is not empty") + } + + return nil +} + +// compressMove 压缩迁移 +func (ms *MigrationService) compressMove(ctx context.Context, srcPath, dstPath string) error { + tempZipFile := filepath.Join(os.TempDir(), + fmt.Sprintf("voidraft_migration_%d.zip", time.Now().UnixNano())) + + defer os.Remove(tempZipFile) // 压缩源目录 - progress.Progress = 40 - ms.updateProgress(*progress) + ms.updateProgress(MigrationProgress{ + Status: MigrationStatusMigrating, + Progress: 40, + }) if err := ms.compressDirectory(ctx, srcPath, tempZipFile); err != nil { - return fmt.Errorf("Failed to compress source directory") + return fmt.Errorf("failed to compress source directory: %v", err) } // 解压到目标位置 - progress.Progress = 70 - ms.updateProgress(*progress) + ms.updateProgress(MigrationProgress{ + Status: MigrationStatusMigrating, + Progress: 70, + }) if err := ms.extractToDirectory(ctx, tempZipFile, dstPath); err != nil { - return fmt.Errorf("Failed to extract to target location") + return fmt.Errorf("failed to extract to target location: %v", err) } // 检查是否取消 @@ -220,13 +250,12 @@ func (ms *MigrationService) atomicCompressMove(ctx context.Context, srcPath, dst } // 删除源目录 - progress.Progress = 90 - ms.updateProgress(*progress) - - if err := os.RemoveAll(srcPath); err != nil { - ms.logger.Error("Migration: Failed to remove source directory", "error", err) - } + ms.updateProgress(MigrationProgress{ + Status: MigrationStatusMigrating, + Progress: 90, + }) + os.RemoveAll(srcPath) return nil } @@ -234,7 +263,7 @@ func (ms *MigrationService) atomicCompressMove(ctx context.Context, srcPath, dst func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFile string) error { zipWriter, err := os.Create(zipFile) if err != nil { - return fmt.Errorf("Failed to create temporary file") + return err } defer zipWriter.Close() @@ -254,21 +283,16 @@ func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFi } relPath, err := filepath.Rel(srcDir, filePath) - if err != nil { + if err != nil || relPath == "." { return err } - if relPath == "." { - return nil - } - header, err := zip.FileInfoHeader(info) if err != nil { return err } header.Name = strings.ReplaceAll(relPath, string(filepath.Separator), "/") - if info.IsDir() { header.Name += "/" header.Method = zip.Store @@ -282,32 +306,34 @@ func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFi } if !info.IsDir() { - file, err := os.Open(filePath) - if err != nil { - return err - } - defer file.Close() - - _, err = io.Copy(writer, file) - if err != nil { - return err - } + return ms.copyFileToZip(filePath, writer) } - return nil }) } +// copyFileToZip 复制文件到zip +func (ms *MigrationService) copyFileToZip(filePath string, writer io.Writer) error { + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(writer, file) + return err +} + // extractToDirectory 从zip文件解压到目录 func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dstDir string) error { reader, err := zip.OpenReader(zipFile) if err != nil { - return fmt.Errorf("Failed to open temporary file") + return err } defer reader.Close() if err := os.MkdirAll(dstDir, 0755); err != nil { - return fmt.Errorf("Failed to create target directory") + return err } for _, file := range reader.File { @@ -318,25 +344,7 @@ func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dst default: } - dstPath := filepath.Join(dstDir, file.Name) - - // 安全检查:防止zip slip攻击 - if !strings.HasPrefix(dstPath, filepath.Clean(dstDir)+string(os.PathSeparator)) { - return fmt.Errorf("Invalid file path in archive") - } - - if file.FileInfo().IsDir() { - if err := os.MkdirAll(dstPath, file.FileInfo().Mode()); err != nil { - return err - } - continue - } - - if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { - return err - } - - if err := ms.extractFile(file, dstPath); err != nil { + if err := ms.extractSingleFile(file, dstDir); err != nil { return err } } @@ -344,8 +352,23 @@ func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dst return nil } -// extractFile 解压单个文件 -func (ms *MigrationService) extractFile(file *zip.File, dstPath string) error { +// extractSingleFile 解压单个文件 +func (ms *MigrationService) extractSingleFile(file *zip.File, dstDir string) error { + dstPath := filepath.Join(dstDir, file.Name) + + // 安全检查:防止zip slip攻击 + if !strings.HasPrefix(dstPath, filepath.Clean(dstDir)+string(os.PathSeparator)) { + return fmt.Errorf("invalid file path in archive: %s", file.Name) + } + + if file.FileInfo().IsDir() { + return os.MkdirAll(dstPath, file.FileInfo().Mode()) + } + + if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { + return err + } + srcFile, err := file.Open() if err != nil { return err @@ -371,20 +394,14 @@ func (ms *MigrationService) isDirectoryEmpty(dirPath string) (bool, error) { defer f.Close() _, err = f.Readdir(1) - if err == io.EOF { - return true, nil - } - if err != nil { - return false, err - } - return false, nil + return err == io.EOF, nil } // isSubDirectory 检查target是否是parent的子目录 func (ms *MigrationService) isSubDirectory(parent, target string) bool { parent = filepath.Clean(parent) + string(filepath.Separator) target = filepath.Clean(target) + string(filepath.Separator) - return len(target) > len(parent) && target[:len(parent)] == parent + return len(target) > len(parent) && strings.HasPrefix(target, parent) } // CancelMigration 取消迁移 @@ -392,21 +409,16 @@ func (ms *MigrationService) CancelMigration() error { ms.mu.Lock() defer ms.mu.Unlock() - if ms.cancelFunc != nil { - ms.cancelFunc() - ms.logger.Info("Migration: Cancellation requested") + if ms.cancel != nil { + ms.cancel() return nil } - return fmt.Errorf("No active migration to cancel") + return fmt.Errorf("no active migration to cancel") } // ServiceShutdown 服务关闭 func (ms *MigrationService) ServiceShutdown() error { - ms.logger.Info("Migration: Service is shutting down...") - if err := ms.CancelMigration(); err != nil { - ms.logger.Debug("Migration: No active migration to cancel during shutdown") - } - ms.logger.Info("Migration: Service shutdown completed") + ms.CancelMigration() return nil } diff --git a/internal/services/path_manager.go b/internal/services/path_manager.go new file mode 100644 index 0000000..ac4b30f --- /dev/null +++ b/internal/services/path_manager.go @@ -0,0 +1,62 @@ +package services + +import ( + "os" + "path/filepath" +) + +// PathManager 路径管理器 +type PathManager struct { + configDir string // 配置目录 + settingsPath string // 设置文件路径 + keybindsPath string // 快捷键配置文件路径 +} + +// NewPathManager 创建新的路径管理器 +func NewPathManager() *PathManager { + // 获取用户配置目录 + userConfigDir, err := os.UserConfigDir() + if err != nil { + // 如果获取失败,使用当前目录 + userConfigDir, _ = os.Getwd() + } + + // 设置voidraft配置目录 + configDir := filepath.Join(userConfigDir, ".voidraft", "config") + + return &PathManager{ + configDir: configDir, + settingsPath: filepath.Join(configDir, "settings.json"), + keybindsPath: filepath.Join(configDir, "keybindings.json"), + } +} + +// GetConfigDir 获取配置目录路径 +func (pm *PathManager) GetConfigDir() string { + return pm.configDir +} + +// GetSettingsPath 获取设置文件路径 +func (pm *PathManager) GetSettingsPath() string { + return pm.settingsPath +} + +// GetKeybindsPath 获取快捷键配置文件路径 +func (pm *PathManager) GetKeybindsPath() string { + return pm.keybindsPath +} + +// EnsureConfigDir 确保配置目录存在 +func (pm *PathManager) EnsureConfigDir() error { + return os.MkdirAll(pm.configDir, 0755) +} + +// GetConfigName 获取配置文件 +func (pm *PathManager) GetConfigName() string { + return "settings" +} + +// GetKeybindsName 获取快捷键配置文件名 +func (pm *PathManager) GetKeybindsName() string { + return "keybindings" +} diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index d5cd95f..23e460e 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -9,6 +9,7 @@ import ( // ServiceManager 服务管理器,负责协调各个服务 type ServiceManager struct { + pathManager *PathManager configService *ConfigService documentService *DocumentService migrationService *MigrationService @@ -26,8 +27,11 @@ func NewServiceManager() *ServiceManager { // 初始化日志服务 logger := log.New() - // 初始化配置服务 - 使用固定配置(当前目录下的 config/config.yaml) - configService := NewConfigService(logger) + // 初始化路径管理器 + pathManager := NewPathManager() + + // 初始化配置服务 + configService := NewConfigService(logger, pathManager) // 初始化迁移服务 migrationService := NewMigrationService(logger) @@ -48,7 +52,7 @@ func NewServiceManager() *ServiceManager { trayService := NewTrayService(logger, configService) // 初始化快捷键服务 - keyBindingService := NewKeyBindingService(logger) + keyBindingService := NewKeyBindingService(logger, pathManager) // 初始化开机启动服务 startupService := NewStartupService(configService, logger) @@ -58,7 +62,6 @@ func NewServiceManager() *ServiceManager { return hotkeyService.UpdateHotkey(enable, hotkey) }) if err != nil { - logger.Error("Failed to set hotkey change callback", "error", err) panic(err) } @@ -67,14 +70,12 @@ func NewServiceManager() *ServiceManager { return documentService.OnDataPathChanged(oldPath, newPath) }) if err != nil { - logger.Error("Failed to set data path change callback", "error", err) panic(err) } // 初始化文档服务 err = documentService.Initialize() if err != nil { - logger.Error("Failed to initialize document service", "error", err) panic(err) } diff --git a/internal/services/startup_windows.go b/internal/services/startup_windows.go index 420f1e1..0de8645 100644 --- a/internal/services/startup_windows.go +++ b/internal/services/startup_windows.go @@ -184,7 +184,7 @@ func (w *WindowsStartupImpl) SetEnabled(enabled bool) error { } } -// setRegistryStartup 设置注册表启动项(备用方法) +// setRegistryStartup 设置注册表启动项 func (w *WindowsStartupImpl) setRegistryStartup(enabled bool) error { key, err := w.openRegistryKey() if err != nil { diff --git a/internal/services/store_service.go b/internal/services/store_service.go index 57c86d5..16fb53a 100644 --- a/internal/services/store_service.go +++ b/internal/services/store_service.go @@ -3,29 +3,29 @@ package services import ( "encoding/json" "fmt" - "io" "os" "path/filepath" "sync" + "sync/atomic" "github.com/wailsapp/wails/v3/pkg/services/log" ) // StoreOption 存储服务配置选项 type StoreOption struct { - FilePath string // 存储文件路径 - AutoSave bool // 是否自动保存 + FilePath string + AutoSave bool Logger *log.LoggerService } // Store 泛型存储服务 type Store[T any] struct { - option StoreOption - data T - dataMap map[string]any - unsaved bool - lock sync.RWMutex - logger *log.LoggerService + option StoreOption + data atomic.Value // stores T + dataMap sync.Map // thread-safe map + unsaved atomic.Bool + initOnce sync.Once + logger *log.LoggerService } // NewStore 存储服务 @@ -35,127 +35,152 @@ func NewStore[T any](option StoreOption) *Store[T] { logger = log.New() } - // 确保目录存在 - if option.FilePath != "" { - dir := filepath.Dir(option.FilePath) - if err := os.MkdirAll(dir, 0755); err != nil { - logger.Error("store: Failed to create directory", "path", dir, "error", err) - } - } - store := &Store[T]{ - option: option, - dataMap: make(map[string]any), - logger: logger, + option: option, + logger: logger, } - // 加载数据 - if err := store.load(); err != nil { - logger.Error("store: Failed to load data", "error", err) - } + // 异步初始化 + store.initOnce.Do(func() { + store.initialize() + }) return store } -// load 加载数据 -func (s *Store[T]) load() error { - if s.option.FilePath == "" { - return fmt.Errorf("store: FilePath not set") - } - - // 如果文件不存在 - if _, err := os.Stat(s.option.FilePath); os.IsNotExist(err) { - return nil - } - - file, err := os.Open(s.option.FilePath) - if err != nil { - return fmt.Errorf("store: Failed to open file: %w", err) - } - defer file.Close() - - bytes, err := io.ReadAll(file) - if err != nil { - return fmt.Errorf("store: Failed to read file: %w", err) - } - - if len(bytes) == 0 { - return nil - } - - s.lock.Lock() - defer s.lock.Unlock() - - if err := json.Unmarshal(bytes, &s.data); err != nil { - // 尝试加载为map格式 - if err := json.Unmarshal(bytes, &s.dataMap); err != nil { - return fmt.Errorf("store: Failed to parse data: %w", err) +// initialize 初始化存储 +func (s *Store[T]) initialize() { + // 确保目录存在 + if s.option.FilePath != "" { + if err := os.MkdirAll(filepath.Dir(s.option.FilePath), 0755); err != nil { + s.logger.Error("store: failed to create directory", "error", err) + return } } - return nil + // 加载数据 + s.load() +} + +// load 加载数据 +func (s *Store[T]) load() { + if s.option.FilePath == "" { + return + } + + // 检查文件是否存在 + if _, err := os.Stat(s.option.FilePath); os.IsNotExist(err) { + return + } + + data, err := os.ReadFile(s.option.FilePath) + if err != nil { + s.logger.Error("store: failed to read file", "error", err) + return + } + + if len(data) == 0 { + return + } + + var value T + if err := json.Unmarshal(data, &value); err != nil { + // 尝试加载为map格式 + var mapData map[string]any + if err := json.Unmarshal(data, &mapData); err != nil { + s.logger.Error("store: failed to parse data", "error", err) + return + } + // 将map数据存储到sync.Map中 + for k, v := range mapData { + s.dataMap.Store(k, v) + } + return + } + + s.data.Store(value) } // Save 保存数据 func (s *Store[T]) Save() error { - s.lock.Lock() - defer s.lock.Unlock() - - err := s.saveInternal() - if err != nil { - s.logger.Error("store: Failed to save", "error", err) - return fmt.Errorf("store: Failed to save: %w", err) + if !s.unsaved.Load() { + return nil // 没有未保存的更改 } - s.unsaved = false + if err := s.saveInternal(); err != nil { + return fmt.Errorf("store: failed to save: %w", err) + } + + s.unsaved.Store(false) return nil } // saveInternal 内部保存实现 func (s *Store[T]) saveInternal() error { if s.option.FilePath == "" { - return fmt.Errorf("store: FilePath not set") + return fmt.Errorf("store: filepath not set") } - // 创建临时文件 + // 获取要保存的数据 + var data []byte + var err error + + if value := s.data.Load(); value != nil { + data, err = json.MarshalIndent(value, "", " ") + } else { + // 如果没有结构化数据,保存map数据 + mapData := make(map[string]any) + s.dataMap.Range(func(key, value any) bool { + if k, ok := key.(string); ok { + mapData[k] = value + } + return true + }) + data, err = json.MarshalIndent(mapData, "", " ") + } + + if err != nil { + return fmt.Errorf("failed to serialize data: %w", err) + } + + // 原子写入 + return s.atomicWrite(data) +} + +// atomicWrite 原子写入文件 +func (s *Store[T]) atomicWrite(data []byte) error { dir := filepath.Dir(s.option.FilePath) + + // 创建临时文件 tempFile, err := os.CreateTemp(dir, "store-*.tmp") if err != nil { - return fmt.Errorf("store: Failed to create temp file: %w", err) + return fmt.Errorf("failed to create temp file: %w", err) } - tempFilePath := tempFile.Name() + + tempPath := tempFile.Name() defer func() { - _ = tempFile.Close() - // 如果出错,删除临时文件 + tempFile.Close() if err != nil { - _ = os.Remove(tempFilePath) + os.Remove(tempPath) } }() - // 序列化数据 - bytes, err := json.MarshalIndent(s.data, "", " ") - if err != nil { - return fmt.Errorf("store: Failed to serialize data: %w", err) + // 写入数据并同步 + if _, err = tempFile.Write(data); err != nil { + return fmt.Errorf("failed to write data: %w", err) } - // 写入临时文件 - if _, err = tempFile.Write(bytes); err != nil { - return fmt.Errorf("store: Failed to write temp file: %w", err) - } - - // 确保所有数据已写入磁盘 if err = tempFile.Sync(); err != nil { - return fmt.Errorf("store: Failed to sync file: %w", err) + return fmt.Errorf("failed to sync file: %w", err) } - // 关闭临时文件 if err = tempFile.Close(); err != nil { - return fmt.Errorf("store: Failed to close temp file: %w", err) + return fmt.Errorf("failed to close temp file: %w", err) } - // 原子替换文件 - if err = os.Rename(tempFilePath, s.option.FilePath); err != nil { - return fmt.Errorf("store: Failed to rename file: %w", err) + // 原子替换 + if err = os.Rename(tempPath, s.option.FilePath); err != nil { + return fmt.Errorf("failed to rename file: %w", err) } return nil @@ -163,70 +188,67 @@ func (s *Store[T]) saveInternal() error { // Get 获取数据 func (s *Store[T]) Get() T { - s.lock.RLock() - defer s.lock.RUnlock() - return s.data + if value := s.data.Load(); value != nil { + return value.(T) + } + var zero T + return zero } // GetProperty 获取指定属性 func (s *Store[T]) GetProperty(key string) any { - s.lock.RLock() - defer s.lock.RUnlock() - if key == "" { - return s.dataMap + // 返回所有map数据 + result := make(map[string]any) + s.dataMap.Range(func(k, v any) bool { + if str, ok := k.(string); ok { + result[str] = v + } + return true + }) + return result } - return s.dataMap[key] + + if value, ok := s.dataMap.Load(key); ok { + return value + } + return nil } // Set 设置数据 func (s *Store[T]) Set(data T) error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data = data - s.unsaved = true + s.data.Store(data) + s.unsaved.Store(true) if s.option.AutoSave { return s.saveInternal() } - return nil } // SetProperty 设置指定属性 func (s *Store[T]) SetProperty(key string, value any) error { - s.lock.Lock() - defer s.lock.Unlock() - - s.dataMap[key] = value - s.unsaved = true + s.dataMap.Store(key, value) + s.unsaved.Store(true) if s.option.AutoSave { return s.saveInternal() } - return nil } // Delete 删除指定属性 func (s *Store[T]) Delete(key string) error { - s.lock.Lock() - defer s.lock.Unlock() - - delete(s.dataMap, key) - s.unsaved = true + s.dataMap.Delete(key) + s.unsaved.Store(true) if s.option.AutoSave { return s.saveInternal() } - return nil } // HasUnsavedChanges 是否有未保存的更改 func (s *Store[T]) HasUnsavedChanges() bool { - s.lock.RLock() - defer s.lock.RUnlock() - return s.unsaved + return s.unsaved.Load() } diff --git a/internal/services/tray_service.go b/internal/services/tray_service.go index 92bae90..37cd0c7 100644 --- a/internal/services/tray_service.go +++ b/internal/services/tray_service.go @@ -31,7 +31,6 @@ func (ts *TrayService) SetAppReferences(app *application.App, mainWindow *applic func (ts *TrayService) ShouldMinimizeToTray() bool { config, err := ts.configService.GetConfig() if err != nil { - ts.logger.Error("TrayService: Failed to get config", "error", err) return true // 默认行为:隐藏到托盘 } @@ -44,11 +43,9 @@ func (ts *TrayService) HandleWindowClose() { // 隐藏到托盘 ts.mainWindow.Hide() ts.app.EmitEvent("window:hidden", nil) - ts.logger.Info("TrayService: Window hidden to system tray") } else { // 直接退出应用 ts.app.Quit() - ts.logger.Info("TrayService: Application quit") } } @@ -58,10 +55,6 @@ func (ts *TrayService) HandleWindowMinimize() { // 隐藏到托盘 ts.mainWindow.Hide() ts.app.EmitEvent("window:hidden", nil) - ts.logger.Info("TrayService: Window minimized to system tray") - } else { - // 允许正常最小化(不处理,让系统处理) - ts.logger.Info("TrayService: Window minimized normally") } } @@ -74,7 +67,6 @@ func (ts *TrayService) ShowWindow() { if ts.app != nil { ts.app.EmitEvent("window:shown", nil) } - ts.logger.Info("TrayService: Window shown from system tray") } } @@ -82,5 +74,4 @@ func (ts *TrayService) ShowWindow() { func (ts *TrayService) MinimizeButtonClicked() { // 最小化按钮总是执行正常最小化到任务栏,不隐藏到托盘 ts.mainWindow.Minimise() - ts.logger.Info("TrayService: Window minimized to taskbar via titlebar button") } diff --git a/main.go b/main.go index a9b74a1..6b7bf31 100644 --- a/main.go +++ b/main.go @@ -73,7 +73,6 @@ func main() { // 'Mac' options tailor the window when running on macOS. // 'BackgroundColour' is the background colour of the window. // 'URL' is the URL that will be loaded into the webview. - log.Println("Creating main window...") mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ Title: "voidraft", Width: 700, @@ -106,7 +105,6 @@ func main() { hotkeyService := serviceManager.GetHotkeyService() err := hotkeyService.Initialize(app) if err != nil { - log.Printf("Failed to initialize hotkey service: %v\n", err) panic(err) }