🎨 Optimize code

This commit is contained in:
2025-06-22 15:08:38 +08:00
parent 35c89e086e
commit eb9b037f8e
22 changed files with 937 additions and 1906 deletions

View File

@@ -975,9 +975,32 @@ export enum TabType {
* UpdatesConfig 更新设置配置 * UpdatesConfig 更新设置配置
*/ */
export class UpdatesConfig { export class UpdatesConfig {
/**
* 当前版本号
*/
"Version": string;
/**
* 是否自动更新
*/
"autoUpdate": boolean;
/**
* 是否启用测试版
*/
"betaChannel": boolean;
/** Creates a new UpdatesConfig instance. */ /** Creates a new UpdatesConfig instance. */
constructor($$source: Partial<UpdatesConfig> = {}) { constructor($$source: Partial<UpdatesConfig> = {}) {
if (!("Version" in $$source)) {
this["Version"] = "";
}
if (!("autoUpdate" in $$source)) {
this["autoUpdate"] = false;
}
if (!("betaChannel" in $$source)) {
this["betaChannel"] = false;
}
Object.assign(this, $$source); Object.assign(this, $$source);
} }

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
/** /**
* HotkeyService 全局热键服务 * HotkeyService Windows全局热键服务
* @module * @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"; import * as models$0 from "../models/models.js";
/** /**
* GetCurrentHotkey 获取当前注册的热键 * GetCurrentHotkey 获取当前热键
*/ */
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } { export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2572811187) as any; let $resultPromise = $Call.ByID(2572811187) as any;
@@ -38,7 +38,7 @@ export function Initialize(app: application$0.App | null): Promise<void> & { can
} }
/** /**
* IsRegistered 检查是否已注册热键 * IsRegistered 检查是否已注册
*/ */
export function IsRegistered(): Promise<boolean> & { cancel(): void } { export function IsRegistered(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(106954156) as any; let $resultPromise = $Call.ByID(106954156) as any;
@@ -54,21 +54,13 @@ export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise<voi
} }
/** /**
* ServiceShutdown 关闭热键服务 * ServiceShutdown 关闭服务
*/ */
export function ServiceShutdown(): Promise<void> & { cancel(): void } { export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(157291181) as any; let $resultPromise = $Call.ByID(157291181) as any;
return $resultPromise; return $resultPromise;
} }
/**
* ToggleWindow 切换窗口显示/隐藏 - 通过事件通知前端处理
*/
export function ToggleWindow(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1318185132) as any;
return $resultPromise;
}
/** /**
* UnregisterHotkey 取消注册全局热键 * UnregisterHotkey 取消注册全局热键
*/ */

View File

@@ -14,34 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as models$0 from "../models/models.js"; import * as models$0 from "../models/models.js";
/**
* DisableKeyBinding 禁用快捷键
*/
export function DisableKeyBinding(command: models$0.KeyBindingCommand): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1594003006, command) as any;
return $resultPromise;
}
/**
* EnableKeyBinding 启用快捷键
*/
export function EnableKeyBinding(command: models$0.KeyBindingCommand): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1462644129, command) as any;
return $resultPromise;
}
/**
* ExportKeyBindings 导出快捷键配置
*/
export function ExportKeyBindings(): Promise<models$0.KeyBinding[]> & { 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 获取所有快捷键配置 * GetAllKeyBindings 获取所有快捷键配置
*/ */
@@ -55,22 +27,10 @@ export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel()
} }
/** /**
* GetKeyBindingByCommand 根据命令获取快捷键 * GetKeyBindingConfig 获取完整快捷键配置
*/ */
export function GetKeyBindingByCommand(command: models$0.KeyBindingCommand): Promise<models$0.KeyBinding | null> & { cancel(): void } { export function GetKeyBindingConfig(): Promise<models$0.KeyBindingConfig | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3066982544, command) as any; let $resultPromise = $Call.ByID(3804318356) 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<models$0.KeyBindingCategory[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3141399810) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType3($result); return $$createType3($result);
}) as any; }) as any;
@@ -79,65 +39,15 @@ export function GetKeyBindingCategories(): Promise<models$0.KeyBindingCategory[]
} }
/** /**
* GetKeyBindingConfig 获取完整快捷键配置 * Shutdown 关闭服务
*/ */
export function GetKeyBindingConfig(): Promise<models$0.KeyBindingConfig | null> & { cancel(): void } { export function Shutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3804318356) as any; let $resultPromise = $Call.ByID(3046465148) 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<models$0.KeyBinding[]> & { 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<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(642201520, keyBindings) as any;
return $resultPromise;
}
/**
* ResetAllKeyBindings 重置所有快捷键到默认值
*/
export function ResetAllKeyBindings(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2771372645) as any;
return $resultPromise;
}
/**
* ResetKeyBinding 重置快捷键到默认值
*/
export function ResetKeyBinding(command: models$0.KeyBindingCommand): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3466323405, command) as any;
return $resultPromise;
}
/**
* UpdateKeyBinding 更新快捷键
*/
export function UpdateKeyBinding(command: models$0.KeyBindingCommand, newKey: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1469368983, command, newKey) as any;
return $resultPromise; return $resultPromise;
} }
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.KeyBinding.createFrom; const $$createType0 = models$0.KeyBinding.createFrom;
const $$createType1 = $Create.Array($$createType0); const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Nullable($$createType0); const $$createType2 = models$0.KeyBindingConfig.createFrom;
const $$createType3 = $Create.Array($Create.Any); const $$createType3 = $Create.Nullable($$createType2);
const $$createType4 = models$0.KeyBindingConfig.createFrom;
const $$createType5 = $Create.Nullable($$createType4);

View File

@@ -76,19 +76,8 @@ export class MemoryStats {
* MigrationProgress 迁移进度信息 * MigrationProgress 迁移进度信息
*/ */
export class MigrationProgress { export class MigrationProgress {
/**
* 迁移状态
*/
"status": MigrationStatus; "status": MigrationStatus;
/**
* 进度百分比 (0-100)
*/
"progress": number; "progress": number;
/**
* 错误信息
*/
"error"?: string; "error"?: string;
/** Creates a new MigrationProgress instance. */ /** Creates a new MigrationProgress instance. */
@@ -121,18 +110,7 @@ export enum MigrationStatus {
*/ */
$zero = "", $zero = "",
/**
* 迁移中
*/
MigrationStatusMigrating = "migrating", MigrationStatusMigrating = "migrating",
/**
* 完成
*/
MigrationStatusCompleted = "completed", MigrationStatusCompleted = "completed",
/**
* 失败
*/
MigrationStatusFailed = "failed", MigrationStatusFailed = "failed",
}; };

View File

@@ -143,7 +143,11 @@ const DEFAULT_CONFIG: AppConfig = {
language: LanguageType.LangZhCN, language: LanguageType.LangZhCN,
systemTheme: SystemThemeType.SystemThemeAuto systemTheme: SystemThemeType.SystemThemeAuto
}, },
updates: {}, updates: {
Version: "1.0.0",
autoUpdate: true,
betaChannel: false
},
metadata: { metadata: {
lastUpdated: new Date().toString() lastUpdated: new Date().toString()
} }

View File

@@ -1,6 +1,8 @@
package models package models
import ( import (
"os"
"path/filepath"
"time" "time"
) )
@@ -82,7 +84,9 @@ type AppearanceConfig struct {
// UpdatesConfig 更新设置配置 // UpdatesConfig 更新设置配置
type UpdatesConfig struct { type UpdatesConfig struct {
// 预留给未来的更新配置 Version string `json:"Version"` // 当前版本号
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
BetaChannel bool `json:"betaChannel"` // 是否启用测试版
} }
// AppConfig 应用配置 - 按照前端设置页面分类组织 // AppConfig 应用配置 - 按照前端设置页面分类组织
@@ -101,10 +105,14 @@ type ConfigMetadata struct {
// NewDefaultAppConfig 创建默认应用配置 // NewDefaultAppConfig 创建默认应用配置
func NewDefaultAppConfig() *AppConfig { func NewDefaultAppConfig() *AppConfig {
currentDir, _ := os.UserConfigDir()
dataDir := filepath.Join(currentDir, ".voidraft", "data")
return &AppConfig{ return &AppConfig{
General: GeneralConfig{ General: GeneralConfig{
AlwaysOnTop: false, AlwaysOnTop: false,
DataPath: "./data", DataPath: dataDir,
EnableSystemTray: true, EnableSystemTray: true,
StartAtLogin: false, StartAtLogin: false,
EnableGlobalHotkey: false, EnableGlobalHotkey: false,
@@ -133,7 +141,11 @@ func NewDefaultAppConfig() *AppConfig {
Language: LangZhCN, Language: LangZhCN,
SystemTheme: SystemThemeAuto, // 默认使用深色系统主题 SystemTheme: SystemThemeAuto, // 默认使用深色系统主题
}, },
Updates: UpdatesConfig{}, Updates: UpdatesConfig{
Version: "1.0.0",
AutoUpdate: true,
BetaChannel: false,
},
Metadata: ConfigMetadata{ Metadata: ConfigMetadata{
LastUpdated: time.Now().Format(time.RFC3339), LastUpdated: time.Now().Format(time.RFC3339),
}, },

View File

@@ -1,10 +1,10 @@
package services package services
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"sync" "sync"
"time" "time"
"voidraft/internal/models" "voidraft/internal/models"
@@ -24,7 +24,7 @@ const (
) )
// ConfigChangeCallback 配置变更回调函数类型 // ConfigChangeCallback 配置变更回调函数类型
type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig interface{}) error type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error
// ConfigListener 配置监听器 // ConfigListener 配置监听器
type ConfigListener struct { type ConfigListener struct {
@@ -32,98 +32,68 @@ type ConfigListener struct {
ChangeType ConfigChangeType // 监听的配置变更类型 ChangeType ConfigChangeType // 监听的配置变更类型
Callback ConfigChangeCallback // 回调函数(现在包含新旧配置) Callback ConfigChangeCallback // 回调函数(现在包含新旧配置)
DebounceDelay time.Duration // 防抖延迟时间 DebounceDelay time.Duration // 防抖延迟时间
GetConfigFunc func(*viper.Viper) interface{} // 获取相关配置的函数 GetConfigFunc func(*viper.Viper) *models.AppConfig // 获取相关配置的函数
// 内部状态 // 内部状态
mu sync.RWMutex // 监听器状态锁 mu sync.RWMutex // 监听器状态锁
timer *time.Timer // 防抖定时器 timer *time.Timer // 防抖定时器
lastConfigHash string // 上次配置的哈希值,用于变更检测 lastConfigHash string // 上次配置的哈希值,用于变更检测
lastConfig interface{} // 上次的配置副本 lastConfig *models.AppConfig // 上次的配置副本
stopChan chan struct{} // 停止通道用于停止异步goroutine ctx context.Context
cancel context.CancelFunc
} }
// ConfigNotificationService 配置通知服务 // ConfigNotificationService 配置通知服务
type ConfigNotificationService struct { type ConfigNotificationService struct {
listeners map[ConfigChangeType]*ConfigListener // 监听器映射 listeners sync.Map // 使用sync.Map替代普通map+锁
mu sync.RWMutex // 读写锁
logger *log.LoggerService // 日志服务 logger *log.LoggerService // 日志服务
viper *viper.Viper // Viper实例 viper *viper.Viper // Viper实例
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
} }
// NewConfigNotificationService 创建配置通知服务 // NewConfigNotificationService 创建配置通知服务
func NewConfigNotificationService(viper *viper.Viper, logger *log.LoggerService) *ConfigNotificationService { func NewConfigNotificationService(viper *viper.Viper, logger *log.LoggerService) *ConfigNotificationService {
ctx, cancel := context.WithCancel(context.Background())
return &ConfigNotificationService{ return &ConfigNotificationService{
listeners: make(map[ConfigChangeType]*ConfigListener),
logger: logger, logger: logger,
viper: viper, viper: viper,
ctx: ctx,
cancel: cancel,
} }
} }
// RegisterListener 注册配置监听器 // RegisterListener 注册配置监听器
func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error { func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error {
cns.mu.Lock() // 清理已存在的监听器
defer cns.mu.Unlock() if existingValue, loaded := cns.listeners.LoadAndDelete(listener.ChangeType); loaded {
if existing, ok := existingValue.(interface{ cancel() }); ok {
// 检查是否已存在同类型监听器 existing.cancel()
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)
} }
// 初始化新监听器 // 初始化新监听器
listener.stopChan = make(chan struct{}) listener.ctx, listener.cancel = context.WithCancel(cns.ctx)
// 初始化监听器的配置状态
if err := cns.initializeListenerState(listener); err != nil { if err := cns.initializeListenerState(listener); err != nil {
cns.logger.Error("ConfigNotification: Failed to initialize listener state", listener.cancel()
"listener", listener.Name,
"error", err)
return fmt.Errorf("failed to initialize listener state: %w", err) return fmt.Errorf("failed to initialize listener state: %w", err)
} }
cns.listeners[listener.ChangeType] = listener cns.listeners.Store(listener.ChangeType, listener)
cns.logger.Info("ConfigNotification: Registered listener",
"name", listener.Name,
"type", string(listener.ChangeType))
return nil 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 初始化监听器状态 // initializeListenerState 初始化监听器状态
func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigListener) error { func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigListener) error {
if listener.GetConfigFunc == nil { if listener.GetConfigFunc == nil {
return fmt.Errorf("GetConfigFunc is required") return fmt.Errorf("GetConfigFunc is required")
} }
// 获取初始配置 if config := listener.GetConfigFunc(cns.viper); config != nil {
config := listener.GetConfigFunc(cns.viper)
if config != nil {
listener.mu.Lock() listener.mu.Lock()
listener.lastConfig = cns.deepCopy(config) listener.lastConfig = deepCopyConfig(config)
listener.lastConfigHash = cns.computeConfigHash(config) listener.lastConfigHash = computeConfigHash(config)
listener.mu.Unlock() listener.mu.Unlock()
} }
@@ -132,205 +102,92 @@ func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigLi
// UnregisterListener 注销配置监听器 // UnregisterListener 注销配置监听器
func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType) { func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType) {
cns.mu.Lock() if value, loaded := cns.listeners.LoadAndDelete(changeType); loaded {
defer cns.mu.Unlock() if listener, ok := value.(*ConfigListener); ok {
listener.cancel()
if listener, exists := cns.listeners[changeType]; exists { }
cns.cleanupListener(listener)
delete(cns.listeners, changeType)
cns.logger.Info("ConfigNotification: Unregistered listener", "type", string(changeType))
} }
} }
// CheckConfigChanges 检查配置变更并通知相关监听器 // CheckConfigChanges 检查配置变更并通知相关监听器
func (cns *ConfigNotificationService) CheckConfigChanges() { func (cns *ConfigNotificationService) CheckConfigChanges() {
cns.mu.RLock() cns.listeners.Range(func(key, value interface{}) bool {
listeners := make([]*ConfigListener, 0, len(cns.listeners)) if listener, ok := value.(*ConfigListener); ok {
for _, listener := range cns.listeners { cns.checkAndNotify(listener)
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)
}
} }
return true
})
} }
// checkListenerConfigChanges 检查单个监听器的配置变更 // checkAndNotify 检查配置变更并通知
func (cns *ConfigNotificationService) checkListenerConfigChanges(listener *ConfigListener) (bool, interface{}, interface{}) { func (cns *ConfigNotificationService) checkAndNotify(listener *ConfigListener) {
if listener.GetConfigFunc == nil { if listener.GetConfigFunc == nil {
return false, nil, nil return
} }
// 获取当前配置
currentConfig := listener.GetConfigFunc(cns.viper) currentConfig := listener.GetConfigFunc(cns.viper)
// 读取监听器状态
listener.mu.RLock() listener.mu.RLock()
lastHash := listener.lastConfigHash lastHash := listener.lastConfigHash
lastConfig := listener.lastConfig lastConfig := listener.lastConfig
listener.mu.RUnlock() listener.mu.RUnlock()
if currentConfig == nil { var hasChanges bool
// 配置不存在或获取失败 var currentHash string
if lastConfig != nil {
// 配置被删除,更新状态 if currentConfig != nil {
listener.mu.Lock() currentHash = computeConfigHash(currentConfig)
listener.lastConfig = nil hasChanges = currentHash != lastHash
listener.lastConfigHash = "" } else {
listener.mu.Unlock() hasChanges = lastConfig != nil
return true, lastConfig, nil
}
return false, nil, nil
} }
// 计算当前配置的哈希 if hasChanges {
currentHash := cns.computeConfigHash(currentConfig)
// 检查是否有变更
if currentHash != lastHash {
// 更新监听器状态
listener.mu.Lock() listener.mu.Lock()
listener.lastConfig = cns.deepCopy(currentConfig) listener.lastConfig = deepCopyConfig(currentConfig)
listener.lastConfigHash = currentHash listener.lastConfigHash = currentHash
listener.mu.Unlock() listener.mu.Unlock()
return true, lastConfig, currentConfig cns.debounceNotify(listener, lastConfig, currentConfig)
}
} }
return false, nil, nil // computeConfigHash 计算配置的哈希值
} func computeConfigHash(config *models.AppConfig) string {
// computeConfigHash 计算配置的哈希值 - 安全稳定版本
func (cns *ConfigNotificationService) computeConfigHash(config interface{}) string {
if config == nil { if config == nil {
return "" return ""
} }
// 使用JSON序列化确保稳定性和准确性
jsonBytes, err := json.Marshal(config) jsonBytes, err := json.Marshal(config)
if err != nil { if err != nil {
// 如果JSON序列化失败回退到字符串表示 return fmt.Sprintf("%p", config)
cns.logger.Warning("ConfigNotification: JSON marshal failed, using string representation",
"error", err)
configStr := fmt.Sprintf("%+v", config)
jsonBytes = []byte(configStr)
} }
hash := sha256.Sum256(jsonBytes) hash := sha256.Sum256(jsonBytes)
return fmt.Sprintf("%x", hash) return fmt.Sprintf("%x", hash)
} }
// deepCopy 深拷贝配置对象 - 完整实现 // deepCopyConfig 深拷贝配置对象
func (cns *ConfigNotificationService) deepCopy(src interface{}) interface{} { func deepCopyConfig(src *models.AppConfig) *models.AppConfig {
if src == nil { if src == nil {
return nil return nil
} }
// 首先尝试JSON序列化方式深拷贝适用于大多数配置对象
jsonBytes, err := json.Marshal(src) jsonBytes, err := json.Marshal(src)
if err != nil { 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 return src
} }
var dst models.AppConfig
if err := json.Unmarshal(jsonBytes, &dst); err != nil {
return src
} }
// debounceNotifyWithChanges 防抖通知(带变更信息)- 修复内存泄漏 return &dst
func (cns *ConfigNotificationService) debounceNotifyWithChanges(listener *ConfigListener, oldConfig, newConfig interface{}) { }
// debounceNotify 防抖通知
func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) {
listener.mu.Lock() listener.mu.Lock()
defer listener.mu.Unlock() defer listener.mu.Unlock()
@@ -340,87 +197,61 @@ func (cns *ConfigNotificationService) debounceNotifyWithChanges(listener *Config
} }
// 创建配置副本,避免在闭包中持有原始引用 // 创建配置副本,避免在闭包中持有原始引用
oldConfigCopy := cns.deepCopy(oldConfig) oldConfigCopy := deepCopyConfig(oldConfig)
newConfigCopy := cns.deepCopy(newConfig) newConfigCopy := deepCopyConfig(newConfig)
// 获取监听器信息的副本
listenerName := listener.Name
changeType := listener.ChangeType changeType := listener.ChangeType
stopChan := listener.stopChan
// 设置新的防抖定时器
listener.timer = time.AfterFunc(listener.DebounceDelay, func() { listener.timer = time.AfterFunc(listener.DebounceDelay, func() {
cns.logger.Debug("ConfigNotification: Executing callback after debounce", cns.executeCallback(listener.ctx, changeType, listener.Callback, oldConfigCopy, newConfigCopy)
"listener", listenerName, })
"type", string(changeType)) }
// 启动独立的goroutine处理回调带有超时和停止信号检查 // executeCallback 执行回调函数
func (cns *ConfigNotificationService) executeCallback(
ctx context.Context,
changeType ConfigChangeType,
callback ConfigChangeCallback,
oldConfig, newConfig *models.AppConfig,
) {
cns.wg.Add(1)
go func() { go func() {
defer cns.wg.Done()
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
cns.logger.Error("ConfigNotification: Callback panic recovered", cns.logger.Error("config callback panic", "error", r)
"listener", listenerName,
"type", string(changeType),
"panic", r)
} }
}() }()
// 检查是否收到停止信号 callbackCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
select { defer cancel()
case <-stopChan:
cns.logger.Debug("ConfigNotification: Callback cancelled due to stop signal",
"listener", listenerName)
return
default:
}
// 执行回调,设置超时 done := make(chan error, 1)
callbackDone := make(chan error, 1)
go func() { go func() {
callbackDone <- listener.Callback(changeType, oldConfigCopy, newConfigCopy) done <- callback(changeType, oldConfig, newConfig)
}() }()
select { select {
case <-stopChan: case <-callbackCtx.Done():
cns.logger.Debug("ConfigNotification: Callback interrupted by stop signal", cns.logger.Error("config callback timeout")
"listener", listenerName) case err := <-done:
return
case err := <-callbackDone:
if err != nil { if err != nil {
cns.logger.Error("ConfigNotification: Callback execution failed", cns.logger.Error("config callback error", "error", err)
"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")
} }
}() }()
})
cns.logger.Debug("ConfigNotification: Debounce timer scheduled",
"listener", listenerName,
"delay", listener.DebounceDelay)
} }
// Cleanup 清理所有监听器 // Cleanup 清理所有监听器
func (cns *ConfigNotificationService) Cleanup() { func (cns *ConfigNotificationService) Cleanup() {
cns.mu.Lock() cns.cancel() // 取消所有context
defer cns.mu.Unlock()
for changeType, listener := range cns.listeners { cns.listeners.Range(func(key, value interface{}) bool {
cns.cleanupListener(listener) cns.listeners.Delete(key)
delete(cns.listeners, changeType) return true
} })
cns.logger.Info("ConfigNotification: All listeners cleaned up") cns.wg.Wait() // 等待所有协程完成
} }
// CreateHotkeyListener 创建热键配置监听器 // CreateHotkeyListener 创建热键配置监听器
@@ -428,20 +259,16 @@ func CreateHotkeyListener(callback func(enable bool, hotkey *models.HotkeyCombo)
return &ConfigListener{ return &ConfigListener{
Name: "HotkeyListener", Name: "HotkeyListener",
ChangeType: ConfigChangeTypeHotkey, ChangeType: ConfigChangeTypeHotkey,
Callback: func(changeType ConfigChangeType, oldConfig, newConfig interface{}) error { Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
// 处理新配置 if newConfig != nil {
if newAppConfig, ok := newConfig.(*models.AppConfig); ok { return callback(newConfig.General.EnableGlobalHotkey, &newConfig.General.GlobalHotkey)
return callback(newAppConfig.General.EnableGlobalHotkey, &newAppConfig.General.GlobalHotkey)
} }
// 如果新配置为空,说明配置被删除,使用默认 // 使用默认配置
if newConfig == nil {
defaultConfig := models.NewDefaultAppConfig() defaultConfig := models.NewDefaultAppConfig()
return callback(defaultConfig.General.EnableGlobalHotkey, &defaultConfig.General.GlobalHotkey) return callback(defaultConfig.General.EnableGlobalHotkey, &defaultConfig.General.GlobalHotkey)
}
return nil
}, },
DebounceDelay: 200 * time.Millisecond, DebounceDelay: 200 * time.Millisecond,
GetConfigFunc: func(v *viper.Viper) interface{} { GetConfigFunc: func(v *viper.Viper) *models.AppConfig {
var config models.AppConfig var config models.AppConfig
if err := v.Unmarshal(&config); err != nil { if err := v.Unmarshal(&config); err != nil {
return nil return nil
@@ -456,31 +283,27 @@ func CreateDataPathListener(callback func(oldPath, newPath string) error) *Confi
return &ConfigListener{ return &ConfigListener{
Name: "DataPathListener", Name: "DataPathListener",
ChangeType: ConfigChangeTypeDataPath, ChangeType: ConfigChangeTypeDataPath,
Callback: func(changeType ConfigChangeType, oldConfig, newConfig interface{}) error { Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
var oldPath, newPath string var oldPath, newPath string
// 处理旧配置 if oldConfig != nil {
if oldAppConfig, ok := oldConfig.(*models.AppConfig); ok { oldPath = oldConfig.General.DataPath
oldPath = oldAppConfig.General.DataPath
} }
// 处理新配置 if newConfig != nil {
if newAppConfig, ok := newConfig.(*models.AppConfig); ok { newPath = newConfig.General.DataPath
newPath = newAppConfig.General.DataPath } else {
} else if newConfig == nil {
// 如果新配置为空,说明配置被删除,使用默认值
defaultConfig := models.NewDefaultAppConfig() defaultConfig := models.NewDefaultAppConfig()
newPath = defaultConfig.General.DataPath newPath = defaultConfig.General.DataPath
} }
// 只有路径真正改变时才调用回调
if oldPath != newPath { if oldPath != newPath {
return callback(oldPath, newPath) return callback(oldPath, newPath)
} }
return nil return nil
}, },
DebounceDelay: 100 * time.Millisecond, // 较短的防抖延迟,因为数据路径变更需要快速响应 DebounceDelay: 100 * time.Millisecond,
GetConfigFunc: func(v *viper.Viper) interface{} { GetConfigFunc: func(v *viper.Viper) *models.AppConfig {
var config models.AppConfig var config models.AppConfig
if err := v.Unmarshal(&config); err != nil { if err := v.Unmarshal(&config); err != nil {
return nil return nil

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "time"
"voidraft/internal/models" "voidraft/internal/models"
@@ -19,6 +18,7 @@ import (
type ConfigService struct { type ConfigService struct {
viper *viper.Viper // Viper 实例 viper *viper.Viper // Viper 实例
logger *log.LoggerService // 日志服务 logger *log.LoggerService // 日志服务
pathManager *PathManager // 路径管理器
mu sync.RWMutex // 读写锁 mu sync.RWMutex // 读写锁
// 配置通知服务 // 配置通知服务
@@ -49,29 +49,24 @@ func (e *ConfigError) Is(target error) bool {
} }
// NewConfigService 创建新的配置服务实例 // NewConfigService 创建新的配置服务实例
func NewConfigService(logger *log.LoggerService) *ConfigService { func NewConfigService(logger *log.LoggerService, pathManager *PathManager) *ConfigService {
// 设置日志服务 // 设置日志服务
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
// 获取当前工作目录 // 设置路径管理器
currentDir, err := os.Getwd() if pathManager == nil {
if err != nil { pathManager = NewPathManager()
currentDir = "."
} }
// 固定配置路径和文件名
configPath := filepath.Join(currentDir, "config")
configName := "settings"
// 创建 Viper 实例 // 创建 Viper 实例
v := viper.New() v := viper.New()
// 配置 Viper // 配置 Viper
v.SetConfigName(configName) v.SetConfigName(pathManager.GetConfigName())
v.SetConfigType("json") v.SetConfigType("json")
v.AddConfigPath(configPath) v.AddConfigPath(pathManager.GetConfigDir())
// 设置环境变量前缀 // 设置环境变量前缀
v.SetEnvPrefix("VOIDRAFT") v.SetEnvPrefix("VOIDRAFT")
@@ -84,6 +79,7 @@ func NewConfigService(logger *log.LoggerService) *ConfigService {
service := &ConfigService{ service := &ConfigService{
viper: v, viper: v,
logger: logger, logger: logger,
pathManager: pathManager,
} }
// 初始化配置通知服务 // 初始化配置通知服务
@@ -91,7 +87,7 @@ func NewConfigService(logger *log.LoggerService) *ConfigService {
// 初始化配置 // 初始化配置
if err := service.initConfig(); err != nil { 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 var configFileNotFoundError viper.ConfigFileNotFoundError
if errors.As(err, &configFileNotFoundError) { if errors.As(err, &configFileNotFoundError) {
// 配置文件不存在,创建默认配置文件 // 配置文件不存在,创建默认配置文件
cs.logger.Info("Config: Config file not found, creating default config")
return cs.createDefaultConfig() return cs.createDefaultConfig()
} }
// 配置文件存在但读取失败 // 配置文件存在但读取失败
return &ConfigError{Operation: "read_config", Err: err} return &ConfigError{Operation: "read_config", Err: err}
} }
cs.logger.Info("Config: Successfully loaded config file", "file", cs.viper.ConfigFileUsed())
return nil return nil
} }
// createDefaultConfig 创建默认配置文件 // createDefaultConfig 创建默认配置文件
func (cs *ConfigService) createDefaultConfig() error { 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} 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} 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} return &ConfigError{Operation: "read_created_config", Err: err}
} }
cs.logger.Info("Config: Created default config file", "path", configPath)
return nil return nil
} }
@@ -196,15 +181,12 @@ func (cs *ConfigService) createDefaultConfig() error {
func (cs *ConfigService) startWatching() { func (cs *ConfigService) startWatching() {
// 设置配置变化回调 // 设置配置变化回调
cs.viper.OnConfigChange(func(e fsnotify.Event) { 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.notificationService.CheckConfigChanges()
}) })
// 启动配置文件监听 // 启动配置文件监听
cs.viper.WatchConfig() cs.viper.WatchConfig()
cs.logger.Info("Config: Started watching config file for changes")
} }
// GetConfig 获取完整应用配置 // GetConfig 获取完整应用配置
@@ -261,30 +243,20 @@ func (cs *ConfigService) ResetConfig() {
// 直接写入JSON文件 // 直接写入JSON文件
if err := cs.writeConfigToFile(defaultConfig); err != nil { if err := cs.writeConfigToFile(defaultConfig); err != nil {
cs.logger.Error("Config: Failed to write config during reset", "error", err)
return return
} }
// 重新读取配置文件到viper // 重新读取配置文件到viper
if err := cs.viper.ReadInConfig(); err != nil { if err := cs.viper.ReadInConfig(); err != nil {
cs.logger.Error("Config: Failed to reload config after reset", "error", err)
return return
} }
cs.logger.Info("Config: All settings have been reset to defaults")
// 手动触发配置变更检查,确保通知系统能感知到变更 // 手动触发配置变更检查,确保通知系统能感知到变更
cs.notificationService.CheckConfigChanges() cs.notificationService.CheckConfigChanges()
} }
// writeConfigToFile 直接写入配置到JSON文件 // writeConfigToFile 直接写入配置到JSON文件
func (cs *ConfigService) writeConfigToFile(config *models.AppConfig) error { func (cs *ConfigService) writeConfigToFile(config *models.AppConfig) error {
// 获取配置文件路径
currentDir, err := os.Getwd()
if err != nil {
currentDir = "."
}
configPath := filepath.Join(currentDir, "config", "settings.json")
// 序列化为JSON // 序列化为JSON
configBytes, err := json.MarshalIndent(config, "", " ") configBytes, err := json.MarshalIndent(config, "", " ")
if err != nil { 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) return fmt.Errorf("failed to write config file: %v", err)
} }

View File

@@ -26,7 +26,6 @@ func NewDialogService(logger *log.LoggerService) *DialogService {
// SetWindow 设置绑定的窗口 // SetWindow 设置绑定的窗口
func (ds *DialogService) SetWindow(window *application.WebviewWindow) { func (ds *DialogService) SetWindow(window *application.WebviewWindow) {
ds.window = window ds.window = window
ds.logger.Info("Dialog service window binding updated")
} }
// SelectDirectory 打开目录选择对话框 // SelectDirectory 打开目录选择对话框
@@ -65,10 +64,7 @@ func (ds *DialogService) SelectDirectory() (string, error) {
path, err := dialog.PromptForSingleSelection() path, err := dialog.PromptForSingleSelection()
if err != nil { if err != nil {
ds.logger.Error("Failed to select directory", "error", err)
return "", err return "", err
} }
ds.logger.Info("Directory selected", "path", path)
return path, nil return path, nil
} }

View File

@@ -1,9 +1,11 @@
package services package services
import ( import (
"context"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic"
"time" "time"
"voidraft/internal/models" "voidraft/internal/models"
@@ -14,15 +16,21 @@ import (
type DocumentService struct { type DocumentService struct {
configService *ConfigService configService *ConfigService
logger *log.LoggerService logger *log.LoggerService
document *models.Document
docStore *Store[models.Document] docStore *Store[models.Document]
mutex sync.RWMutex
// 自动保存优化 // 文档状态管理
saveTimer *time.Timer mu sync.RWMutex
isDirty bool document *models.Document
lastSaveTime time.Time
pendingContent string // 暂存待保存的内容 // 自动保存管理
ctx context.Context
cancel context.CancelFunc
isDirty atomic.Bool
lastSaveTime atomic.Int64 // unix timestamp
saveScheduler chan struct{}
// 初始化控制
initOnce sync.Once
} }
// NewDocumentService 创建文档服务 // NewDocumentService 创建文档服务
@@ -31,20 +39,33 @@ func NewDocumentService(configService *ConfigService, logger *log.LoggerService)
logger = log.New() logger = log.New()
} }
ctx, cancel := context.WithCancel(context.Background())
return &DocumentService{ return &DocumentService{
configService: configService, configService: configService,
logger: logger, logger: logger,
ctx: ctx,
cancel: cancel,
saveScheduler: make(chan struct{}, 1),
} }
} }
// Initialize 初始化服务 // Initialize 初始化服务
func (ds *DocumentService) Initialize() error { 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 { if err := ds.initStore(); err != nil {
return err return err
} }
ds.loadDocument() ds.loadDocument()
ds.startAutoSave() go ds.autoSaveWorker()
return nil return nil
} }
@@ -55,7 +76,6 @@ func (ds *DocumentService) initStore() error {
return err return err
} }
// 确保目录存在
if err := os.MkdirAll(filepath.Dir(docPath), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(docPath), 0755); err != nil {
return err return err
} }
@@ -75,72 +95,75 @@ func (ds *DocumentService) getDocumentPath() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return filepath.Join(config.General.DataPath, "docs", "default.json"), nil return filepath.Join(config.General.DataPath, "docs", "default.json"), nil
} }
// loadDocument 加载文档 // loadDocument 加载文档
func (ds *DocumentService) loadDocument() { func (ds *DocumentService) loadDocument() {
ds.mutex.Lock() ds.mu.Lock()
defer ds.mutex.Unlock() defer ds.mu.Unlock()
doc := ds.docStore.Get() doc := ds.docStore.Get()
if doc.Meta.ID == "" { if doc.Meta.ID == "" {
// 创建新文档
ds.document = models.NewDefaultDocument() ds.document = models.NewDefaultDocument()
ds.docStore.Set(*ds.document) ds.docStore.Set(*ds.document)
ds.logger.Info("Document: Created new document")
} else { } else {
ds.document = &doc ds.document = &doc
ds.logger.Info("Document: Loaded existing document")
} }
ds.lastSaveTime.Store(time.Now().Unix())
} }
// GetActiveDocument 获取活动文档 // GetActiveDocument 获取活动文档
func (ds *DocumentService) GetActiveDocument() (*models.Document, error) { func (ds *DocumentService) GetActiveDocument() (*models.Document, error) {
ds.mutex.RLock() ds.mu.RLock()
defer ds.mutex.RUnlock() defer ds.mu.RUnlock()
if ds.document == nil { if ds.document == nil {
return nil, nil return nil, nil
} }
// 返回副本
docCopy := *ds.document docCopy := *ds.document
return &docCopy, nil return &docCopy, nil
} }
// UpdateActiveDocumentContent 更新文档内容 // UpdateActiveDocumentContent 更新文档内容
func (ds *DocumentService) UpdateActiveDocumentContent(content string) error { func (ds *DocumentService) UpdateActiveDocumentContent(content string) error {
ds.mutex.Lock() ds.mu.Lock()
defer ds.mutex.Unlock() defer ds.mu.Unlock()
if ds.document != nil { if ds.document != nil && ds.document.Content != content {
// 只在内容真正改变时才标记为脏 ds.document.Content = content
if ds.document.Content != content { ds.markDirty()
ds.pendingContent = content
ds.isDirty = true
}
} }
return nil return nil
} }
// markDirty 标记为脏数据并触发自动保存
func (ds *DocumentService) markDirty() {
if ds.isDirty.CompareAndSwap(false, true) {
select {
case ds.saveScheduler <- struct{}{}:
default: // 已有保存任务在队列中
}
}
}
// ForceSave 强制保存 // ForceSave 强制保存
func (ds *DocumentService) ForceSave() error { func (ds *DocumentService) ForceSave() error {
ds.mutex.Lock() return ds.saveDocument()
defer ds.mutex.Unlock() }
// saveDocument 保存文档
func (ds *DocumentService) saveDocument() error {
ds.mu.Lock()
defer ds.mu.Unlock()
if ds.document == nil { if ds.document == nil {
return nil return nil
} }
// 应用待保存的内容
if ds.pendingContent != "" {
ds.document.Content = ds.pendingContent
ds.pendingContent = ""
}
now := time.Now() now := time.Now()
ds.document.Meta.LastUpdated = now ds.document.Meta.LastUpdated = now
@@ -152,85 +175,71 @@ func (ds *DocumentService) ForceSave() error {
return err return err
} }
ds.isDirty = false ds.isDirty.Store(false)
ds.lastSaveTime = now ds.lastSaveTime.Store(now.Unix())
ds.logger.Info("Document: Force save completed")
return nil return nil
} }
// startAutoSave 启动自动保存 // autoSaveWorker 自动保存工作协程
func (ds *DocumentService) startAutoSave() { func (ds *DocumentService) autoSaveWorker() {
delay := 5 * time.Second // 默认延迟 ticker := time.NewTicker(ds.getAutoSaveInterval())
defer ticker.Stop()
if config, err := ds.configService.GetConfig(); err == nil { for {
delay = time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond select {
} case <-ds.ctx.Done():
return
ds.scheduleAutoSave(delay) case <-ds.saveScheduler:
}
// scheduleAutoSave 安排自动保存
func (ds *DocumentService) scheduleAutoSave(delay time.Duration) {
if ds.saveTimer != nil {
ds.saveTimer.Stop()
}
ds.saveTimer = time.AfterFunc(delay, func() {
ds.performAutoSave() ds.performAutoSave()
ds.startAutoSave() // 重新安排 case <-ticker.C:
}) if ds.isDirty.Load() {
ds.performAutoSave()
}
// 动态调整保存间隔
ticker.Reset(ds.getAutoSaveInterval())
}
}
}
// getAutoSaveInterval 获取自动保存间隔
func (ds *DocumentService) getAutoSaveInterval() time.Duration {
config, err := ds.configService.GetConfig()
if err != nil {
return 5 * time.Second
}
return time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond
} }
// performAutoSave 执行自动保存 // performAutoSave 执行自动保存
func (ds *DocumentService) performAutoSave() { func (ds *DocumentService) performAutoSave() {
ds.mutex.Lock() if !ds.isDirty.Load() {
defer ds.mutex.Unlock()
if !ds.isDirty || ds.document == nil {
return return
} }
// 检查距离上次保存的时间间隔,避免过于频繁的保存 // 防抖:避免过于频繁的保存
now := time.Now() lastSave := time.Unix(ds.lastSaveTime.Load(), 0)
if now.Sub(ds.lastSaveTime) < time.Second { if time.Since(lastSave) < time.Second {
// 如果距离上次保存不到1秒跳过此次保存 // 延迟重试
// 下一个自动保存周期会重新尝试 time.AfterFunc(time.Second, func() {
ds.logger.Debug("Document: Skipping auto save due to recent save") select {
case ds.saveScheduler <- struct{}{}:
default:
}
})
return return
} }
// 应用待保存的内容 if err := ds.saveDocument(); err != nil {
if ds.pendingContent != "" { ds.logger.Error("auto save failed", "error", err)
ds.document.Content = ds.pendingContent
ds.pendingContent = ""
} }
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 重新加载文档 // ReloadDocument 重新加载文档
func (ds *DocumentService) ReloadDocument() error { func (ds *DocumentService) ReloadDocument() error {
ds.mutex.Lock() // 先保存当前文档
defer ds.mutex.Unlock() if ds.isDirty.Load() {
if err := ds.saveDocument(); err != nil {
// 强制保存当前文档 return err
if ds.document != nil && ds.isDirty {
if err := ds.forceSaveInternal(); err != nil {
ds.logger.Error("Document: Failed to save before reload", "error", err)
} }
} }
@@ -239,76 +248,24 @@ func (ds *DocumentService) ReloadDocument() error {
return err return err
} }
// 重新加载文档 // 重新加载
doc := ds.docStore.Get() ds.loadDocument()
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
return nil return nil
} }
// ServiceShutdown 关闭服务 // ServiceShutdown 关闭服务
func (ds *DocumentService) ServiceShutdown() error { func (ds *DocumentService) ServiceShutdown() error {
// 停止定时器 ds.cancel() // 停止自动保存工作协程
if ds.saveTimer != nil {
ds.saveTimer.Stop()
}
// 最后保存 // 最后保存
if err := ds.ForceSave(); err != nil { if ds.isDirty.Load() {
ds.logger.Error("Document: Failed to save on shutdown", "error", err) return ds.saveDocument()
} }
ds.logger.Info("Document: Service shutdown completed")
return nil return nil
} }
// OnDataPathChanged 处理数据路径变更 // OnDataPathChanged 处理数据路径变更
func (ds *DocumentService) OnDataPathChanged(oldPath, newPath string) error { func (ds *DocumentService) OnDataPathChanged(oldPath, newPath string) error {
ds.logger.Info("Document: Data path changed, reloading document",
"oldPath", oldPath,
"newPath", newPath)
// 重新加载文档以使用新的路径
return ds.ReloadDocument() return ds.ReloadDocument()
} }

View File

@@ -11,8 +11,10 @@ package services
import "C" import "C"
import ( import (
"context"
"fmt" "fmt"
"sync" "sync"
"sync/atomic"
"time" "time"
"voidraft/internal/models" "voidraft/internal/models"
@@ -20,17 +22,19 @@ import (
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
) )
// HotkeyService 全局热键服务 // HotkeyService Windows全局热键服务
type HotkeyService struct { type HotkeyService struct {
logger *log.LoggerService logger *log.LoggerService
configService *ConfigService configService *ConfigService
app *application.App app *application.App
mu sync.RWMutex mu sync.RWMutex
isRegistered bool
currentHotkey *models.HotkeyCombo currentHotkey *models.HotkeyCombo
stopChan chan struct{} isRegistered atomic.Bool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup wg sync.WaitGroup
running bool
} }
// HotkeyError 热键错误 // HotkeyError 热键错误
@@ -39,27 +43,26 @@ type HotkeyError struct {
Err error Err error
} }
// Error 实现error接口
func (e *HotkeyError) Error() string { 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 { func (e *HotkeyError) Unwrap() error {
return e.Err return e.Err
} }
// NewHotkeyService 创建新的热键服务实例 // NewHotkeyService 创建热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService { func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
ctx, cancel := context.WithCancel(context.Background())
return &HotkeyService{ return &HotkeyService{
logger: logger, logger: logger,
configService: configService, configService: configService,
isRegistered: false, ctx: ctx,
running: false, cancel: cancel,
} }
} }
@@ -67,72 +70,57 @@ func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *
func (hs *HotkeyService) Initialize(app *application.App) error { func (hs *HotkeyService) Initialize(app *application.App) error {
hs.app = app hs.app = app
// 加载并应用当前配置
config, err := hs.configService.GetConfig() config, err := hs.configService.GetConfig()
if err != nil { if err != nil {
return &HotkeyError{Operation: "load_config", Err: err} return &HotkeyError{"load_config", err}
} }
if config.General.EnableGlobalHotkey { if config.General.EnableGlobalHotkey {
err = hs.RegisterHotkey(&config.General.GlobalHotkey) if err := hs.RegisterHotkey(&config.General.GlobalHotkey); err != nil {
if err != nil { hs.logger.Error("failed to register startup hotkey", "error", err)
hs.logger.Error("Hotkey: Failed to register hotkey on startup", "error", err)
} }
} }
hs.logger.Info("Hotkey: Service initialized")
return nil return nil
} }
// RegisterHotkey 注册全局热键 // RegisterHotkey 注册全局热键
func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
if !hs.isValidHotkey(hotkey) {
return &HotkeyError{"validate", fmt.Errorf("invalid hotkey combination")}
}
hs.mu.Lock() hs.mu.Lock()
defer hs.mu.Unlock() defer hs.mu.Unlock()
// 取消注册现有热键 // 取消现有热键
if hs.isRegistered { if hs.isRegistered.Load() {
hs.logger.Info("Hotkey: Unregistering existing hotkey before registering new one") hs.unregisterInternal()
err := hs.unregisterHotkeyInternal()
if err != nil {
return err
}
} }
// 验证热键组合 // 启动监听器
if !hs.isValidHotkey(hotkey) { ctx, cancel := context.WithCancel(hs.ctx)
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{})
hs.wg.Add(1) hs.wg.Add(1)
go hs.hotkeyListener(hotkey, readyChan)
// 等待监听器启动完成,设置超时避免无限等待 ready := make(chan error, 1)
go hs.hotkeyListener(ctx, hotkey, ready)
// 等待启动完成
select { select {
case <-readyChan: case err := <-ready:
// 监听器启动完成 if err != nil {
case <-time.After(1 * time.Second): cancel()
// 超时处理 return &HotkeyError{"register", err}
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 <-time.After(time.Second):
cancel()
return &HotkeyError{"register", fmt.Errorf("timeout")}
} }
hs.currentHotkey = hotkey hs.currentHotkey = hotkey
hs.isRegistered = true hs.isRegistered.Store(true)
hs.running = true hs.cancel = cancel
hs.logger.Info("Hotkey: Successfully registered global hotkey")
return nil return nil
} }
@@ -140,204 +128,85 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
func (hs *HotkeyService) UnregisterHotkey() error { func (hs *HotkeyService) UnregisterHotkey() error {
hs.mu.Lock() hs.mu.Lock()
defer hs.mu.Unlock() defer hs.mu.Unlock()
return hs.unregisterInternal()
return hs.unregisterHotkeyInternal()
} }
// unregisterHotkeyInternal 内部取消注册方法(无锁) // unregisterInternal 内部取消注册(无锁)
func (hs *HotkeyService) unregisterHotkeyInternal() error { func (hs *HotkeyService) unregisterInternal() error {
if !hs.isRegistered { if !hs.isRegistered.Load() {
hs.logger.Debug("Hotkey: No hotkey registered, skipping unregister")
return nil return nil
} }
hs.logger.Info("Hotkey: Unregistering global hotkey") if hs.cancel != nil {
hs.cancel()
// 停止监听
if hs.stopChan != nil {
close(hs.stopChan)
hs.logger.Debug("Hotkey: Waiting for listener goroutine to stop")
hs.wg.Wait() hs.wg.Wait()
hs.logger.Debug("Hotkey: Listener goroutine stopped")
} }
// 重置状态
hs.currentHotkey = nil hs.currentHotkey = nil
hs.isRegistered = false hs.isRegistered.Store(false)
hs.running = false
hs.stopChan = nil
hs.logger.Info("Hotkey: Successfully unregistered global hotkey")
return nil return nil
} }
// UpdateHotkey 更新热键配置 // UpdateHotkey 更新热键配置
func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error { 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 { if enable {
// 启用热键直接注册新热键RegisterHotkey 会处理取消旧热键) return hs.RegisterHotkey(hotkey)
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.UnregisterHotkey()
} }
// ToggleWindow 切换窗口显示/隐藏 - 通过事件通知前端处理 // hotkeyListener 热键监听器
func (hs *HotkeyService) ToggleWindow() { func (hs *HotkeyService) hotkeyListener(ctx context.Context, hotkey *models.HotkeyCombo, ready chan<- error) {
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{}) {
defer hs.wg.Done() defer hs.wg.Done()
hs.logger.Debug("Hotkey: Starting Windows API hotkey listener")
// 将热键转换为虚拟键码
mainKeyVK := hs.keyToVirtualKeyCode(hotkey.Key) mainKeyVK := hs.keyToVirtualKeyCode(hotkey.Key)
if mainKeyVK == 0 { if mainKeyVK == 0 {
hs.logger.Error("Hotkey: Invalid key", "key", hotkey.Key) ready <- fmt.Errorf("invalid key: %s", hotkey.Key)
close(readyChan)
return return
} }
// 检查间隔100ms减少CPU使用率
ticker := time.NewTicker(100 * time.Millisecond) ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
// 添加状态跟踪 var wasPressed bool
var wasPressed bool = false ready <- nil // 标记准备就绪
// 标记是否已经发送ready信号确保只发送一次
readySent := false
for { for {
select { select {
case <-hs.stopChan: case <-ctx.Done():
hs.logger.Debug("Hotkey: Stopping Windows API hotkey listener")
if !readySent {
close(readyChan)
}
return return
case <-ticker.C: case <-ticker.C:
// 第一次循环时发送ready信号表示监听器已经准备就绪 ctrl := cBool(hotkey.Ctrl)
if !readySent { shift := cBool(hotkey.Shift)
close(readyChan) alt := cBool(hotkey.Alt)
readySent = true win := cBool(hotkey.Win)
hs.logger.Debug("Hotkey: Listener ready signal sent")
}
// 调用 C 函数检查热键组合 isPressed := C.isHotkeyPressed(ctrl, shift, alt, win, C.int(mainKeyVK)) == 1
ctrl := 0
if hotkey.Ctrl {
ctrl = 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 { if isPressed && !wasPressed {
hs.logger.Debug("Hotkey: Global hotkey triggered via Windows API") hs.toggleWindow()
hs.ToggleWindow()
} }
// 更新状态
wasPressed = isPressed 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 { func (hs *HotkeyService) keyToVirtualKeyCode(key string) int {
keyMap := map[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, "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, "Q": 0x51, "R": 0x52, "S": 0x53, "T": 0x54, "U": 0x55, "V": 0x56, "W": 0x57, "X": 0x58,
"Y": 0x59, "Z": 0x5A, "Y": 0x59, "Z": 0x5A,
// 数字键 // 数字键
"0": 0x30, "1": 0x31, "2": 0x32, "3": 0x33, "4": 0x34, "0": 0x30, "1": 0x31, "2": 0x32, "3": 0x33, "4": 0x34,
"5": 0x35, "6": 0x36, "7": 0x37, "8": 0x38, "9": 0x39, "5": 0x35, "6": 0x36, "7": 0x37, "8": 0x38, "9": 0x39,
// 功能键 // 功能键
"F1": 0x70, "F2": 0x71, "F3": 0x72, "F4": 0x73, "F5": 0x74, "F6": 0x75, "F1": 0x70, "F2": 0x71, "F3": 0x72, "F4": 0x73, "F5": 0x74, "F6": 0x75,
"F7": 0x76, "F8": 0x77, "F9": 0x78, "F10": 0x79, "F11": 0x7A, "F12": 0x7B, "F7": 0x76, "F8": 0x77, "F9": 0x78, "F10": 0x79, "F11": 0x7A, "F12": 0x7B,
} }
return keyMap[key]
if vk, exists := keyMap[key]; exists {
return vk
}
return 0
} }
// isValidHotkey 验证热键组合是否有效 // isValidHotkey 验证热键组合
func (hs *HotkeyService) isValidHotkey(hotkey *models.HotkeyCombo) bool { func (hs *HotkeyService) isValidHotkey(hotkey *models.HotkeyCombo) bool {
if hotkey == nil { if hotkey == nil || hotkey.Key == "" {
return false return false
} }
// 至少需要一个修饰键
// 必须有主键
if hotkey.Key == "" {
return false
}
// 必须至少有一个修饰键
if !hotkey.Ctrl && !hotkey.Shift && !hotkey.Alt && !hotkey.Win { if !hotkey.Ctrl && !hotkey.Shift && !hotkey.Alt && !hotkey.Win {
return false return false
} }
// 验证主键是否在有效范围内
return hs.keyToVirtualKeyCode(hotkey.Key) != 0 return hs.keyToVirtualKeyCode(hotkey.Key) != 0
} }
// GetCurrentHotkey 获取当前注册的热键 // GetCurrentHotkey 获取当前热键
func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
hs.mu.RLock() hs.mu.RLock()
defer hs.mu.RUnlock() defer hs.mu.RUnlock()
@@ -390,7 +245,6 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
return nil return nil
} }
// 返回副本避免并发问题
return &models.HotkeyCombo{ return &models.HotkeyCombo{
Ctrl: hs.currentHotkey.Ctrl, Ctrl: hs.currentHotkey.Ctrl,
Shift: hs.currentHotkey.Shift, Shift: hs.currentHotkey.Shift,
@@ -400,25 +254,14 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
} }
} }
// IsRegistered 检查是否已注册热键 // IsRegistered 检查是否已注册
func (hs *HotkeyService) IsRegistered() bool { func (hs *HotkeyService) IsRegistered() bool {
hs.mu.RLock() return hs.isRegistered.Load()
defer hs.mu.RUnlock()
return hs.isRegistered
} }
// ServiceShutdown 关闭热键服务 // ServiceShutdown 关闭服务
func (hs *HotkeyService) ServiceShutdown() error { func (hs *HotkeyService) ServiceShutdown() error {
hs.mu.Lock() hs.cancel()
defer hs.mu.Unlock() hs.wg.Wait()
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")
return nil return nil
} }

View File

@@ -68,6 +68,7 @@ import "C"
import ( import (
"fmt" "fmt"
"sync" "sync"
"sync/atomic"
"voidraft/internal/models" "voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
@@ -83,7 +84,7 @@ type HotkeyService struct {
configService *ConfigService configService *ConfigService
app *application.App app *application.App
mu sync.RWMutex mu sync.RWMutex
isRegistered bool isRegistered atomic.Bool
currentHotkey *models.HotkeyCombo currentHotkey *models.HotkeyCombo
} }
@@ -95,7 +96,7 @@ type HotkeyError struct {
// Error 实现error接口 // Error 实现error接口
func (e *HotkeyError) Error() string { 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 获取原始错误 // Unwrap 获取原始错误
@@ -112,7 +113,6 @@ func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *
service := &HotkeyService{ service := &HotkeyService{
logger: logger, logger: logger,
configService: configService, configService: configService,
isRegistered: false,
} }
// 设置全局实例 // 设置全局实例
@@ -128,41 +128,31 @@ func (hs *HotkeyService) Initialize(app *application.App) error {
// 加载并应用当前配置 // 加载并应用当前配置
config, err := hs.configService.GetConfig() config, err := hs.configService.GetConfig()
if err != nil { if err != nil {
return &HotkeyError{Operation: "load_config", Err: err} return &HotkeyError{"load_config", err}
} }
if config.General.EnableGlobalHotkey { if config.General.EnableGlobalHotkey {
err = hs.RegisterHotkey(&config.General.GlobalHotkey) if err := hs.RegisterHotkey(&config.General.GlobalHotkey); err != nil {
if err != nil { hs.logger.Error("failed to register startup hotkey", "error", err)
hs.logger.Error("Hotkey: Failed to register hotkey on startup", "error", err)
} }
} }
hs.logger.Info("Hotkey: macOS service initialized")
return nil return nil
} }
// RegisterHotkey 注册全局热键 // RegisterHotkey 注册全局热键
func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
hs.mu.Lock()
defer hs.mu.Unlock()
// 验证热键组合
if !hs.isValidHotkey(hotkey) { 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", hs.mu.Lock()
"ctrl", hotkey.Ctrl, defer hs.mu.Unlock()
"shift", hotkey.Shift,
"alt", hotkey.Alt,
"win", hotkey.Win,
"key", hotkey.Key)
// 转换键码和修饰符 // 转换键码和修饰符
keyCode := hs.keyToMacKeyCode(hotkey.Key) keyCode := hs.keyToMacKeyCode(hotkey.Key)
if keyCode == 0 { 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) modifiers := hs.buildMacModifiers(hotkey)
@@ -170,13 +160,12 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
// 调用C函数注册热键 // 调用C函数注册热键
result := int(C.registerGlobalHotkey(C.int(keyCode), C.int(modifiers))) result := int(C.registerGlobalHotkey(C.int(keyCode), C.int(modifiers)))
if result == 0 { 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.currentHotkey = hotkey
hs.isRegistered = true hs.isRegistered.Store(true)
hs.logger.Info("Hotkey: Successfully registered global hotkey on macOS")
return nil return nil
} }
@@ -185,67 +174,28 @@ func (hs *HotkeyService) UnregisterHotkey() error {
hs.mu.Lock() hs.mu.Lock()
defer hs.mu.Unlock() defer hs.mu.Unlock()
if !hs.isRegistered { if !hs.isRegistered.Load() {
hs.logger.Debug("Hotkey: No hotkey registered, skipping unregister")
return nil return nil
} }
hs.logger.Info("Hotkey: Unregistering global hotkey on macOS")
// 调用C函数取消注册热键 // 调用C函数取消注册热键
result := int(C.unregisterGlobalHotkey()) result := int(C.unregisterGlobalHotkey())
if result == 0 { 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.currentHotkey = nil
hs.isRegistered = false hs.isRegistered.Store(false)
hs.logger.Info("Hotkey: Successfully unregistered global hotkey on macOS")
return nil return nil
} }
// UpdateHotkey 更新热键配置 // UpdateHotkey 更新热键配置
func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error { 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 { if enable {
// 启用热键直接注册新热键RegisterHotkey 会处理取消旧热键) return hs.RegisterHotkey(hotkey)
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 hs.UnregisterHotkey()
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
}
}
// 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)")
} }
// keyToMacKeyCode 将键名转换为macOS虚拟键码 // keyToMacKeyCode 将键名转换为macOS虚拟键码
@@ -330,31 +280,18 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
// IsRegistered 检查是否已注册热键 // IsRegistered 检查是否已注册热键
func (hs *HotkeyService) IsRegistered() bool { func (hs *HotkeyService) IsRegistered() bool {
hs.mu.RLock() return hs.isRegistered.Load()
defer hs.mu.RUnlock()
return hs.isRegistered
} }
// ServiceShutdown 关闭热键服务 // ServiceShutdown 关闭热键服务
func (hs *HotkeyService) ServiceShutdown() error { func (hs *HotkeyService) ServiceShutdown() error {
hs.mu.Lock() return hs.UnregisterHotkey()
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
} }
//export hotkeyTriggered //export hotkeyTriggered
func hotkeyTriggered() { func hotkeyTriggered() {
// 通过全局实例调用ToggleWindow // 通过全局实例调用ToggleWindow
if globalHotkeyService != nil { if globalHotkeyService != nil && globalHotkeyService.app != nil {
globalHotkeyService.ToggleWindow() globalHotkeyService.ToggleWindow()
} }
} }

View File

@@ -127,8 +127,10 @@ int isHotkeyRegistered() {
import "C" import "C"
import ( import (
"context"
"fmt" "fmt"
"sync" "sync"
"sync/atomic"
"time" "time"
"unsafe" "unsafe"
"voidraft/internal/models" "voidraft/internal/models"
@@ -142,12 +144,14 @@ type HotkeyService struct {
logger *log.LoggerService logger *log.LoggerService
configService *ConfigService configService *ConfigService
app *application.App app *application.App
mu sync.RWMutex mu sync.RWMutex
isRegistered bool
currentHotkey *models.HotkeyCombo currentHotkey *models.HotkeyCombo
stopChan chan struct{} isRegistered atomic.Bool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup wg sync.WaitGroup
running bool
} }
// HotkeyError 热键错误 // HotkeyError 热键错误
@@ -158,25 +162,25 @@ type HotkeyError struct {
// Error 实现error接口 // Error 实现error接口
func (e *HotkeyError) Error() string { 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 { func (e *HotkeyError) Unwrap() error {
return e.Err return e.Err
} }
// NewHotkeyService 创建新的热键服务实例 // NewHotkeyService 创建热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService { func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
ctx, cancel := context.WithCancel(context.Background())
return &HotkeyService{ return &HotkeyService{
logger: logger, logger: logger,
configService: configService, configService: configService,
isRegistered: false, ctx: ctx,
running: false, cancel: cancel,
} }
} }
@@ -184,91 +188,73 @@ func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *
func (hs *HotkeyService) Initialize(app *application.App) error { func (hs *HotkeyService) Initialize(app *application.App) error {
hs.app = app hs.app = app
// 初始化X11显示
if int(C.initX11Display()) == 0 { 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() config, err := hs.configService.GetConfig()
if err != nil { if err != nil {
return &HotkeyError{Operation: "load_config", Err: err} return &HotkeyError{"load_config", err}
} }
if config.General.EnableGlobalHotkey { if config.General.EnableGlobalHotkey {
err = hs.RegisterHotkey(&config.General.GlobalHotkey) if err := hs.RegisterHotkey(&config.General.GlobalHotkey); err != nil {
if err != nil { hs.logger.Error("failed to register startup hotkey", "error", err)
hs.logger.Error("Hotkey: Failed to register hotkey on startup", "error", err)
} }
} }
hs.logger.Info("Hotkey: Linux service initialized")
return nil return nil
} }
// RegisterHotkey 注册全局热键 // RegisterHotkey 注册全局热键
func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
if !hs.isValidHotkey(hotkey) {
return &HotkeyError{"validate", fmt.Errorf("invalid hotkey combination")}
}
hs.mu.Lock() hs.mu.Lock()
defer hs.mu.Unlock() defer hs.mu.Unlock()
// 取消注册现有热键 // 取消现有热键
if hs.isRegistered { if hs.isRegistered.Load() {
hs.logger.Info("Hotkey: Unregistering existing hotkey before registering new one") hs.unregisterInternal()
err := hs.unregisterHotkeyInternal()
if err != nil {
return err
}
} }
// 验证热键组合
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) keyCode := hs.keyToX11KeyCode(hotkey.Key)
if keyCode == 0 { 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) modifiers := hs.buildX11Modifiers(hotkey)
// 调用C函数注册热键
result := int(C.registerGlobalHotkey(C.int(keyCode), C.uint(modifiers))) result := int(C.registerGlobalHotkey(C.int(keyCode), C.uint(modifiers)))
if result == 0 { 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{}) ctx, cancel := context.WithCancel(hs.ctx)
// 确保 stopChan 是新的
hs.stopChan = make(chan struct{})
hs.wg.Add(1) hs.wg.Add(1)
go hs.hotkeyListener(hotkey, readyChan)
// 等待监听器启动完成,设置超时避免无限等待 ready := make(chan error, 1)
go hs.hotkeyListener(ctx, ready)
// 等待启动完成
select { select {
case <-readyChan: case err := <-ready:
// 监听器启动完成 if err != nil {
case <-time.After(1 * time.Second): cancel()
// 超时处理 return &HotkeyError{"start_listener", err}
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 <-time.After(time.Second):
cancel()
return &HotkeyError{"start_listener", fmt.Errorf("timeout")}
} }
hs.currentHotkey = hotkey hs.currentHotkey = hotkey
hs.isRegistered = true hs.isRegistered.Store(true)
hs.running = true hs.cancel = cancel
hs.logger.Info("Hotkey: Successfully registered global hotkey on Linux")
return nil return nil
} }
@@ -276,130 +262,70 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
func (hs *HotkeyService) UnregisterHotkey() error { func (hs *HotkeyService) UnregisterHotkey() error {
hs.mu.Lock() hs.mu.Lock()
defer hs.mu.Unlock() defer hs.mu.Unlock()
return hs.unregisterInternal()
return hs.unregisterHotkeyInternal()
} }
// unregisterHotkeyInternal 内部取消注册方法(无锁) // unregisterInternal 内部取消注册(无锁)
func (hs *HotkeyService) unregisterHotkeyInternal() error { func (hs *HotkeyService) unregisterInternal() error {
if !hs.isRegistered { if !hs.isRegistered.Load() {
hs.logger.Debug("Hotkey: No hotkey registered, skipping unregister")
return nil return nil
} }
hs.logger.Info("Hotkey: Unregistering global hotkey on Linux") if hs.cancel != nil {
hs.cancel()
// 停止监听
if hs.stopChan != nil {
close(hs.stopChan)
hs.logger.Debug("Hotkey: Waiting for listener goroutine to stop")
hs.wg.Wait() hs.wg.Wait()
hs.logger.Debug("Hotkey: Listener goroutine stopped")
} }
// 调用C函数取消注册热键
result := int(C.unregisterGlobalHotkey()) result := int(C.unregisterGlobalHotkey())
if result == 0 { 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.currentHotkey = nil
hs.isRegistered = false hs.isRegistered.Store(false)
hs.running = false
hs.stopChan = nil
hs.logger.Info("Hotkey: Successfully unregistered global hotkey on Linux")
return nil return nil
} }
// UpdateHotkey 更新热键配置 // UpdateHotkey 更新热键配置
func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error { 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 { if enable {
// 启用热键直接注册新热键RegisterHotkey 会处理取消旧热键) return hs.RegisterHotkey(hotkey)
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.UnregisterHotkey()
} }
// ToggleWindow 切换窗口显示/隐藏 // hotkeyListener 热键监听器
func (hs *HotkeyService) ToggleWindow() { func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error) {
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{}) {
defer hs.wg.Done() defer hs.wg.Done()
hs.logger.Debug("Hotkey: Starting Linux X11 hotkey listener")
// 检查间隔100ms
ticker := time.NewTicker(100 * time.Millisecond) ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
// 标记是否已经发送ready信号确保只发送一次 ready <- nil // 标记准备就绪
readySent := false
for { for {
select { select {
case <-hs.stopChan: case <-ctx.Done():
hs.logger.Debug("Hotkey: Stopping Linux X11 hotkey listener")
if !readySent {
close(readyChan)
}
return return
case <-ticker.C: case <-ticker.C:
// 第一次循环时发送ready信号表示监听器已经准备就绪
if !readySent {
close(readyChan)
readySent = true
hs.logger.Debug("Hotkey: Listener ready signal sent")
}
// 检查热键事件
if int(C.checkHotkeyEvent()) == 1 { 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 { func (hs *HotkeyService) keyToX11KeyCode(key string) int {
// 将Go字符串转换为C字符串
cKey := C.CString(key) cKey := C.CString(key)
defer C.free(unsafe.Pointer(cKey)) defer C.free(unsafe.Pointer(cKey))
return int(C.getX11Keycode(cKey)) return int(C.getX11Keycode(cKey))
} }
@@ -423,27 +349,19 @@ func (hs *HotkeyService) buildX11Modifiers(hotkey *models.HotkeyCombo) uint {
return modifiers return modifiers
} }
// isValidHotkey 验证热键组合是否有效 // isValidHotkey 验证热键组合
func (hs *HotkeyService) isValidHotkey(hotkey *models.HotkeyCombo) bool { func (hs *HotkeyService) isValidHotkey(hotkey *models.HotkeyCombo) bool {
if hotkey == nil { if hotkey == nil || hotkey.Key == "" {
return false return false
} }
// 至少需要一个修饰键
// 必须有主键
if hotkey.Key == "" {
return false
}
// 必须至少有一个修饰键
if !hotkey.Ctrl && !hotkey.Shift && !hotkey.Alt && !hotkey.Win { if !hotkey.Ctrl && !hotkey.Shift && !hotkey.Alt && !hotkey.Win {
return false return false
} }
// 验证主键是否在有效范围内
return hs.keyToX11KeyCode(hotkey.Key) != 0 return hs.keyToX11KeyCode(hotkey.Key) != 0
} }
// GetCurrentHotkey 获取当前注册的热键 // GetCurrentHotkey 获取当前热键
func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
hs.mu.RLock() hs.mu.RLock()
defer hs.mu.RUnlock() defer hs.mu.RUnlock()
@@ -452,7 +370,6 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
return nil return nil
} }
// 返回副本避免并发问题
return &models.HotkeyCombo{ return &models.HotkeyCombo{
Ctrl: hs.currentHotkey.Ctrl, Ctrl: hs.currentHotkey.Ctrl,
Shift: hs.currentHotkey.Shift, Shift: hs.currentHotkey.Shift,
@@ -462,28 +379,15 @@ func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
} }
} }
// IsRegistered 检查是否已注册热键 // IsRegistered 检查是否已注册
func (hs *HotkeyService) IsRegistered() bool { func (hs *HotkeyService) IsRegistered() bool {
hs.mu.RLock() return hs.isRegistered.Load()
defer hs.mu.RUnlock()
return hs.isRegistered
} }
// ServiceShutdown 关闭热键服务 // ServiceShutdown 关闭服务
func (hs *HotkeyService) ServiceShutdown() error { func (hs *HotkeyService) ServiceShutdown() error {
hs.mu.Lock() hs.cancel()
defer hs.mu.Unlock() hs.wg.Wait()
if hs.isRegistered {
err := hs.unregisterHotkeyInternal()
if err != nil {
hs.logger.Error("Hotkey: Failed to unregister hotkey on shutdown", "error", err)
}
}
// 关闭X11显示
C.closeX11Display() C.closeX11Display()
hs.logger.Info("Hotkey: Linux service shutdown completed")
return nil return nil
} }

View File

@@ -10,7 +10,7 @@ import (
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
) )
// HotkeyService 全局热键服务 // HotkeyService 存根热键服务
type HotkeyService struct { type HotkeyService struct {
logger *log.LoggerService logger *log.LoggerService
configService *ConfigService configService *ConfigService
@@ -24,7 +24,7 @@ type HotkeyError struct {
// Error 实现error接口 // Error 实现error接口
func (e *HotkeyError) Error() string { 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 获取原始错误 // Unwrap 获取原始错误
@@ -32,7 +32,7 @@ func (e *HotkeyError) Unwrap() error {
return e.Err return e.Err
} }
// NewHotkeyService 创建新的热键服务实例 // NewHotkeyService 创建热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService { func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *HotkeyService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
@@ -46,40 +46,39 @@ func NewHotkeyService(configService *ConfigService, logger *log.LoggerService) *
// Initialize 初始化热键服务 // Initialize 初始化热键服务
func (hs *HotkeyService) Initialize(app *application.App) error { 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 return nil
} }
// RegisterHotkey 注册全局热键 // RegisterHotkey 注册全局热键
func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error { 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 取消注册全局热键 // UnregisterHotkey 取消注册全局热键
func (hs *HotkeyService) UnregisterHotkey() error { 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 更新热键配置 // UpdateHotkey 更新热键配置
func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error { func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) error {
if enable { if enable {
return hs.RegisterHotkey(hotkey) return hs.RegisterHotkey(hotkey)
} else { }
return hs.UnregisterHotkey() return hs.UnregisterHotkey()
} }
}
// GetCurrentHotkey 获取当前注册的热键 // GetCurrentHotkey 获取当前热键
func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo { func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
return nil return nil
} }
// IsRegistered 检查是否已注册热键 // IsRegistered 检查是否已注册
func (hs *HotkeyService) IsRegistered() bool { func (hs *HotkeyService) IsRegistered() bool {
return false return false
} }
// ServiceShutdown 关闭热键服务 // ServiceShutdown 关闭服务
func (hs *HotkeyService) ServiceShutdown() error { func (hs *HotkeyService) ServiceShutdown() error {
return nil return nil
} }

View File

@@ -1,14 +1,12 @@
package services package services
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings"
"sync" "sync"
"time"
"voidraft/internal/models" "voidraft/internal/models"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
@@ -18,96 +16,90 @@ import (
// KeyBindingService 快捷键管理服务 // KeyBindingService 快捷键管理服务
type KeyBindingService struct { type KeyBindingService struct {
viper *viper.Viper // Viper 实例 viper *viper.Viper
logger *log.LoggerService // 日志服务 logger *log.LoggerService
mu sync.RWMutex // 读写锁 pathManager *PathManager
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
initOnce sync.Once
} }
// KeyBindingError 快捷键错误 // KeyBindingError 快捷键错误
type KeyBindingError struct { type KeyBindingError struct {
Operation string // 操作名称 Operation string
Command string // 快捷键Command Command string
Err error // 原始错误 Err error
} }
// Error 实现error接口
func (e *KeyBindingError) Error() string { func (e *KeyBindingError) Error() string {
if e.Command != "" { 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 { func (e *KeyBindingError) Unwrap() error {
return e.Err return e.Err
} }
// Is 实现错误匹配
func (e *KeyBindingError) Is(target error) bool { func (e *KeyBindingError) Is(target error) bool {
var keyBindingError *KeyBindingError var keyBindingError *KeyBindingError
ok := errors.As(target, &keyBindingError) return errors.As(target, &keyBindingError)
return ok
} }
// NewKeyBindingService 创建新的快捷键服务实例 // NewKeyBindingService 创建快捷键服务实例
func NewKeyBindingService(logger *log.LoggerService) *KeyBindingService { func NewKeyBindingService(logger *log.LoggerService, pathManager *PathManager) *KeyBindingService {
// 设置日志服务
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
if pathManager == nil {
// 获取当前工作目录 pathManager = NewPathManager()
currentDir, err := os.Getwd()
if err != nil {
currentDir = "."
} }
// 固定配置路径和文件名 ctx, cancel := context.WithCancel(context.Background())
configPath := filepath.Join(currentDir, "config")
configName := "keybindings"
// 创建 Viper 实例 // 创建并配置 Viper
v := viper.New() v := viper.New()
v.SetConfigName(pathManager.GetKeybindsName())
// 配置 Viper
v.SetConfigName(configName)
v.SetConfigType("json") v.SetConfigType("json")
v.AddConfigPath(configPath) v.AddConfigPath(pathManager.GetConfigDir())
// 设置环境变量前缀
v.SetEnvPrefix("VOIDRAFT_KEYBINDING") v.SetEnvPrefix("VOIDRAFT_KEYBINDING")
v.AutomaticEnv() v.AutomaticEnv()
// 设置默认值
setKeyBindingDefaults(v)
// 构造快捷键服务实例
service := &KeyBindingService{ service := &KeyBindingService{
viper: v, viper: v,
logger: logger, logger: logger,
pathManager: pathManager,
ctx: ctx,
cancel: cancel,
} }
// 初始化配置 // 异步初始化
if err := service.initConfig(); err != nil { go service.initialize()
service.logger.Error("KeyBinding: Failed to initialize keybinding config", "error", err)
}
// 启动配置文件监听
service.startWatching()
return service return service
} }
// setKeyBindingDefaults 设置默认快捷键配置 // initialize 初始化配置
func setKeyBindingDefaults(v *viper.Viper) { 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() defaultConfig := models.NewDefaultKeyBindingConfig()
kbs.viper.SetDefault("keyBindings", defaultConfig.KeyBindings)
// 快捷键列表默认值 kbs.viper.SetDefault("metadata.lastUpdated", defaultConfig.Metadata.LastUpdated)
v.SetDefault("keyBindings", defaultConfig.KeyBindings)
// 元数据默认值
v.SetDefault("metadata.lastUpdated", defaultConfig.Metadata.LastUpdated)
} }
// initConfig 初始化配置 // initConfig 初始化配置
@@ -115,68 +107,41 @@ func (kbs *KeyBindingService) initConfig() error {
kbs.mu.Lock() kbs.mu.Lock()
defer kbs.mu.Unlock() defer kbs.mu.Unlock()
// 尝试读取配置文件
if err := kbs.viper.ReadInConfig(); err != nil { if err := kbs.viper.ReadInConfig(); err != nil {
var configFileNotFoundError viper.ConfigFileNotFoundError var configFileNotFoundError viper.ConfigFileNotFoundError
if errors.As(err, &configFileNotFoundError) { if errors.As(err, &configFileNotFoundError) {
// 配置文件不存在,创建默认配置文件
kbs.logger.Info("KeyBinding: Config file not found, creating default keybinding config")
return kbs.createDefaultConfig() return kbs.createDefaultConfig()
} }
// 配置文件存在但读取失败 return &KeyBindingError{"read_config", "", err}
return &KeyBindingError{Operation: "read_keybinding_config", Err: err}
} }
kbs.logger.Info("KeyBinding: Successfully loaded keybinding config file", "file", kbs.viper.ConfigFileUsed())
return nil return nil
} }
// createDefaultConfig 创建默认配置文件 // createDefaultConfig 创建默认配置文件
func (kbs *KeyBindingService) createDefaultConfig() error { func (kbs *KeyBindingService) createDefaultConfig() error {
// 获取配置目录路径 if err := kbs.pathManager.EnsureConfigDir(); err != nil {
currentDir, err := os.Getwd() return &KeyBindingError{"create_config_dir", "", err}
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}
} }
// 获取默认配置
defaultConfig := models.NewDefaultKeyBindingConfig() defaultConfig := models.NewDefaultKeyBindingConfig()
configBytes, err := json.MarshalIndent(defaultConfig, "", " ") configBytes, err := json.MarshalIndent(defaultConfig, "", " ")
if err != nil { if err != nil {
return &KeyBindingError{Operation: "marshal_default_keybinding_config", Err: err} return &KeyBindingError{"marshal_config", "", err}
} }
// 写入配置文件 if err := os.WriteFile(kbs.pathManager.GetKeybindsPath(), configBytes, 0644); err != nil {
if err := os.WriteFile(configPath, configBytes, 0644); err != nil { return &KeyBindingError{"write_config", "", err}
return &KeyBindingError{Operation: "write_default_keybinding_config", Err: err}
} }
// 重新读取配置文件到viper return kbs.viper.ReadInConfig()
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
} }
// startWatching 启动配置文件监听 // startWatching 启动配置文件监听
func (kbs *KeyBindingService) startWatching() { func (kbs *KeyBindingService) startWatching() {
// 设置配置变化回调
kbs.viper.OnConfigChange(func(e fsnotify.Event) { 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.viper.WatchConfig()
kbs.logger.Info("KeyBinding: Started watching keybinding config file for changes")
} }
// GetKeyBindingConfig 获取完整快捷键配置 // GetKeyBindingConfig 获取完整快捷键配置
@@ -186,392 +151,22 @@ func (kbs *KeyBindingService) GetKeyBindingConfig() (*models.KeyBindingConfig, e
var config models.KeyBindingConfig var config models.KeyBindingConfig
if err := kbs.viper.Unmarshal(&config); err != nil { 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 return &config, nil
} }
// GetAllKeyBindings 获取所有快捷键配置 // GetAllKeyBindings 获取所有快捷键配置
func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) { func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) {
kbs.mu.RLock()
defer kbs.mu.RUnlock()
config, err := kbs.GetKeyBindingConfig() config, err := kbs.GetKeyBindingConfig()
if err != nil { if err != nil {
return nil, &KeyBindingError{Operation: "get_all_keybindings", Err: err} return nil, err
} }
return config.KeyBindings, nil return config.KeyBindings, nil
} }
// GetKeyBindingsByCategory 根据分类获取快捷键 // Shutdown 关闭服务
func (kbs *KeyBindingService) GetKeyBindingsByCategory(category models.KeyBindingCategory) ([]models.KeyBinding, error) { func (kbs *KeyBindingService) Shutdown() error {
kbs.mu.RLock() kbs.cancel()
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))
return nil return nil
} }

View File

@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
@@ -18,25 +19,26 @@ import (
type MigrationStatus string type MigrationStatus string
const ( const (
MigrationStatusMigrating MigrationStatus = "migrating" // 迁移中 MigrationStatusMigrating MigrationStatus = "migrating"
MigrationStatusCompleted MigrationStatus = "completed" // 完成 MigrationStatusCompleted MigrationStatus = "completed"
MigrationStatusFailed MigrationStatus = "failed" // 失败 MigrationStatusFailed MigrationStatus = "failed"
) )
// MigrationProgress 迁移进度信息 // MigrationProgress 迁移进度信息
type MigrationProgress struct { type MigrationProgress struct {
Status MigrationStatus `json:"status"` // 迁移状态 Status MigrationStatus `json:"status"`
Progress float64 `json:"progress"` // 进度百分比 (0-100) Progress float64 `json:"progress"`
Error string `json:"error,omitempty"` // 错误信息 Error string `json:"error,omitempty"`
} }
// MigrationService 迁移服务 // MigrationService 迁移服务
type MigrationService struct { type MigrationService struct {
logger *log.LoggerService logger *log.LoggerService
mu sync.RWMutex mu sync.RWMutex
currentProgress MigrationProgress progress atomic.Value // stores MigrationProgress
cancelFunc context.CancelFunc
ctx context.Context ctx context.Context
cancel context.CancelFunc
} }
// NewMigrationService 创建迁移服务 // NewMigrationService 创建迁移服务
@@ -45,27 +47,27 @@ func NewMigrationService(logger *log.LoggerService) *MigrationService {
logger = log.New() logger = log.New()
} }
return &MigrationService{ ms := &MigrationService{
logger: logger, logger: logger,
currentProgress: MigrationProgress{
Status: MigrationStatusCompleted, // 初始状态为完成
Progress: 0,
},
} }
// 初始化进度
ms.progress.Store(MigrationProgress{
Status: MigrationStatusCompleted,
Progress: 0,
})
return ms
} }
// GetProgress 获取当前进度 // GetProgress 获取当前进度
func (ms *MigrationService) GetProgress() MigrationProgress { func (ms *MigrationService) GetProgress() MigrationProgress {
ms.mu.RLock() return ms.progress.Load().(MigrationProgress)
defer ms.mu.RUnlock()
return ms.currentProgress
} }
// updateProgress 更新进度 // updateProgress 更新进度
func (ms *MigrationService) updateProgress(progress MigrationProgress) { func (ms *MigrationService) updateProgress(progress MigrationProgress) {
ms.mu.Lock() ms.progress.Store(progress)
ms.currentProgress = progress
ms.mu.Unlock()
} }
// MigrateDirectory 迁移目录 // MigrateDirectory 迁移目录
@@ -74,71 +76,83 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
ms.mu.Lock() ms.mu.Lock()
ms.ctx = ctx ms.ctx = ctx
ms.cancelFunc = cancel ms.cancel = cancel
ms.mu.Unlock() ms.mu.Unlock()
defer func() { defer func() {
ms.mu.Lock() ms.mu.Lock()
ms.cancelFunc = nil ms.cancel = nil
ms.ctx = nil ms.ctx = nil
ms.mu.Unlock() ms.mu.Unlock()
}() }()
ms.logger.Info("Migration: Starting directory migration", "from", srcPath, "to", dstPath)
// 初始化进度 // 初始化进度
progress := MigrationProgress{ ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating, Status: MigrationStatusMigrating,
Progress: 0, 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) { if _, err := os.Stat(srcPath); os.IsNotExist(err) {
progress.Status = MigrationStatusCompleted return errNoMigrationNeeded
progress.Progress = 100
ms.updateProgress(progress)
return nil
} }
// 如果路径相同,不需要迁移 // 如果路径相同,不需要迁移
srcAbs, _ := filepath.Abs(srcPath) srcAbs, _ := filepath.Abs(srcPath)
dstAbs, _ := filepath.Abs(dstPath) dstAbs, _ := filepath.Abs(dstPath)
if srcAbs == dstAbs { if srcAbs == dstAbs {
progress.Status = MigrationStatusCompleted return errNoMigrationNeeded
progress.Progress = 100
ms.updateProgress(progress)
return nil
} }
// 检查目标路径是否是源路径的子目录 // 检查目标路径是否是源路径的子目录
if ms.isSubDirectory(srcAbs, dstAbs) { 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") 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 return nil
} }
// failWithError 失败并记录错误
func (ms *MigrationService) failWithError(err error) error {
ms.updateProgress(MigrationProgress{
Status: MigrationStatusFailed,
Error: err.Error(),
})
return err
}
// atomicMove 原子移动目录 // 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 { select {
case <-ctx.Done(): 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(filepath.Dir(dstPath), 0755); err != nil {
if err := os.MkdirAll(dstParent, 0755); err != nil { return fmt.Errorf("failed to create target parent directory: %v", err)
return fmt.Errorf("Failed to create target parent directory")
} }
// 检查目标路径情况 // 检查目标路径
if stat, err := os.Stat(dstPath); err == nil { if err := ms.checkTargetPath(dstPath); err != nil {
if !stat.IsDir() { return err
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")
}
} }
// 尝试直接重命名(如果在同一分区,这会很快) // 尝试直接重命名
progress.Progress = 20 ms.updateProgress(MigrationProgress{
ms.updateProgress(*progress) Status: MigrationStatusMigrating,
Progress: 20,
})
if err := os.Rename(srcPath, dstPath); err == nil { if err := os.Rename(srcPath, dstPath); err == nil {
ms.logger.Info("Migration: Fast rename successful")
return nil return nil
} else {
ms.logger.Info("Migration: Fast rename failed, using copy method", "error", err)
} }
// 重命名失败,使用压缩迁移 // 重命名失败,使用压缩迁移
progress.Progress = 30 ms.updateProgress(MigrationProgress{
ms.updateProgress(*progress) Status: MigrationStatusMigrating,
Progress: 30,
})
return ms.atomicCompressMove(ctx, srcPath, dstPath, progress) return ms.compressMove(ctx, srcPath, dstPath)
} }
// atomicCompressMove 原子压缩迁移 // checkTargetPath 检查目标路径
func (ms *MigrationService) atomicCompressMove(ctx context.Context, srcPath, dstPath string, progress *MigrationProgress) error { func (ms *MigrationService) checkTargetPath(dstPath string) error {
tempDir := os.TempDir() stat, err := os.Stat(dstPath)
tempZipFile := filepath.Join(tempDir, fmt.Sprintf("voidraft_migration_%d.zip", time.Now().UnixNano())) if os.IsNotExist(err) {
return nil
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 err != nil {
return fmt.Errorf("failed to check target path: %v", 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(MigrationProgress{
ms.updateProgress(*progress) Status: MigrationStatusMigrating,
Progress: 40,
})
if err := ms.compressDirectory(ctx, srcPath, tempZipFile); err != nil { 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(MigrationProgress{
ms.updateProgress(*progress) Status: MigrationStatusMigrating,
Progress: 70,
})
if err := ms.extractToDirectory(ctx, tempZipFile, dstPath); err != nil { 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(MigrationProgress{
ms.updateProgress(*progress) Status: MigrationStatusMigrating,
Progress: 90,
if err := os.RemoveAll(srcPath); err != nil { })
ms.logger.Error("Migration: Failed to remove source directory", "error", err)
}
os.RemoveAll(srcPath)
return nil 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 { func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFile string) error {
zipWriter, err := os.Create(zipFile) zipWriter, err := os.Create(zipFile)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create temporary file") return err
} }
defer zipWriter.Close() defer zipWriter.Close()
@@ -254,21 +283,16 @@ func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFi
} }
relPath, err := filepath.Rel(srcDir, filePath) relPath, err := filepath.Rel(srcDir, filePath)
if err != nil { if err != nil || relPath == "." {
return err return err
} }
if relPath == "." {
return nil
}
header, err := zip.FileInfoHeader(info) header, err := zip.FileInfoHeader(info)
if err != nil { if err != nil {
return err return err
} }
header.Name = strings.ReplaceAll(relPath, string(filepath.Separator), "/") header.Name = strings.ReplaceAll(relPath, string(filepath.Separator), "/")
if info.IsDir() { if info.IsDir() {
header.Name += "/" header.Name += "/"
header.Method = zip.Store header.Method = zip.Store
@@ -282,6 +306,14 @@ func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFi
} }
if !info.IsDir() { if !info.IsDir() {
return ms.copyFileToZip(filePath, writer)
}
return nil
})
}
// copyFileToZip 复制文件到zip
func (ms *MigrationService) copyFileToZip(filePath string, writer io.Writer) error {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
return err return err
@@ -289,25 +321,19 @@ func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFi
defer file.Close() defer file.Close()
_, err = io.Copy(writer, file) _, err = io.Copy(writer, file)
if err != nil {
return err return err
} }
}
return nil
})
}
// extractToDirectory 从zip文件解压到目录 // extractToDirectory 从zip文件解压到目录
func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dstDir string) error { func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dstDir string) error {
reader, err := zip.OpenReader(zipFile) reader, err := zip.OpenReader(zipFile)
if err != nil { if err != nil {
return fmt.Errorf("Failed to open temporary file") return err
} }
defer reader.Close() defer reader.Close()
if err := os.MkdirAll(dstDir, 0755); err != nil { if err := os.MkdirAll(dstDir, 0755); err != nil {
return fmt.Errorf("Failed to create target directory") return err
} }
for _, file := range reader.File { for _, file := range reader.File {
@@ -318,25 +344,7 @@ func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dst
default: default:
} }
dstPath := filepath.Join(dstDir, file.Name) if err := ms.extractSingleFile(file, dstDir); err != nil {
// 安全检查防止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 {
return err return err
} }
} }
@@ -344,8 +352,23 @@ func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dst
return nil return nil
} }
// extractFile 解压单个文件 // extractSingleFile 解压单个文件
func (ms *MigrationService) extractFile(file *zip.File, dstPath string) error { 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() srcFile, err := file.Open()
if err != nil { if err != nil {
return err return err
@@ -371,20 +394,14 @@ func (ms *MigrationService) isDirectoryEmpty(dirPath string) (bool, error) {
defer f.Close() defer f.Close()
_, err = f.Readdir(1) _, err = f.Readdir(1)
if err == io.EOF { return err == io.EOF, nil
return true, nil
}
if err != nil {
return false, err
}
return false, nil
} }
// isSubDirectory 检查target是否是parent的子目录 // isSubDirectory 检查target是否是parent的子目录
func (ms *MigrationService) isSubDirectory(parent, target string) bool { func (ms *MigrationService) isSubDirectory(parent, target string) bool {
parent = filepath.Clean(parent) + string(filepath.Separator) parent = filepath.Clean(parent) + string(filepath.Separator)
target = filepath.Clean(target) + 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 取消迁移 // CancelMigration 取消迁移
@@ -392,21 +409,16 @@ func (ms *MigrationService) CancelMigration() error {
ms.mu.Lock() ms.mu.Lock()
defer ms.mu.Unlock() defer ms.mu.Unlock()
if ms.cancelFunc != nil { if ms.cancel != nil {
ms.cancelFunc() ms.cancel()
ms.logger.Info("Migration: Cancellation requested")
return nil return nil
} }
return fmt.Errorf("No active migration to cancel") return fmt.Errorf("no active migration to cancel")
} }
// ServiceShutdown 服务关闭 // ServiceShutdown 服务关闭
func (ms *MigrationService) ServiceShutdown() error { func (ms *MigrationService) ServiceShutdown() error {
ms.logger.Info("Migration: Service is shutting down...") ms.CancelMigration()
if err := ms.CancelMigration(); err != nil {
ms.logger.Debug("Migration: No active migration to cancel during shutdown")
}
ms.logger.Info("Migration: Service shutdown completed")
return nil return nil
} }

View File

@@ -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"
}

View File

@@ -9,6 +9,7 @@ import (
// ServiceManager 服务管理器,负责协调各个服务 // ServiceManager 服务管理器,负责协调各个服务
type ServiceManager struct { type ServiceManager struct {
pathManager *PathManager
configService *ConfigService configService *ConfigService
documentService *DocumentService documentService *DocumentService
migrationService *MigrationService migrationService *MigrationService
@@ -26,8 +27,11 @@ func NewServiceManager() *ServiceManager {
// 初始化日志服务 // 初始化日志服务
logger := log.New() logger := log.New()
// 初始化配置服务 - 使用固定配置(当前目录下的 config/config.yaml // 初始化路径管理器
configService := NewConfigService(logger) pathManager := NewPathManager()
// 初始化配置服务
configService := NewConfigService(logger, pathManager)
// 初始化迁移服务 // 初始化迁移服务
migrationService := NewMigrationService(logger) migrationService := NewMigrationService(logger)
@@ -48,7 +52,7 @@ func NewServiceManager() *ServiceManager {
trayService := NewTrayService(logger, configService) trayService := NewTrayService(logger, configService)
// 初始化快捷键服务 // 初始化快捷键服务
keyBindingService := NewKeyBindingService(logger) keyBindingService := NewKeyBindingService(logger, pathManager)
// 初始化开机启动服务 // 初始化开机启动服务
startupService := NewStartupService(configService, logger) startupService := NewStartupService(configService, logger)
@@ -58,7 +62,6 @@ func NewServiceManager() *ServiceManager {
return hotkeyService.UpdateHotkey(enable, hotkey) return hotkeyService.UpdateHotkey(enable, hotkey)
}) })
if err != nil { if err != nil {
logger.Error("Failed to set hotkey change callback", "error", err)
panic(err) panic(err)
} }
@@ -67,14 +70,12 @@ func NewServiceManager() *ServiceManager {
return documentService.OnDataPathChanged(oldPath, newPath) return documentService.OnDataPathChanged(oldPath, newPath)
}) })
if err != nil { if err != nil {
logger.Error("Failed to set data path change callback", "error", err)
panic(err) panic(err)
} }
// 初始化文档服务 // 初始化文档服务
err = documentService.Initialize() err = documentService.Initialize()
if err != nil { if err != nil {
logger.Error("Failed to initialize document service", "error", err)
panic(err) panic(err)
} }

View File

@@ -184,7 +184,7 @@ func (w *WindowsStartupImpl) SetEnabled(enabled bool) error {
} }
} }
// setRegistryStartup 设置注册表启动项(备用方法) // setRegistryStartup 设置注册表启动项
func (w *WindowsStartupImpl) setRegistryStartup(enabled bool) error { func (w *WindowsStartupImpl) setRegistryStartup(enabled bool) error {
key, err := w.openRegistryKey() key, err := w.openRegistryKey()
if err != nil { if err != nil {

View File

@@ -3,28 +3,28 @@ package services
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic"
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
) )
// StoreOption 存储服务配置选项 // StoreOption 存储服务配置选项
type StoreOption struct { type StoreOption struct {
FilePath string // 存储文件路径 FilePath string
AutoSave bool // 是否自动保存 AutoSave bool
Logger *log.LoggerService Logger *log.LoggerService
} }
// Store 泛型存储服务 // Store 泛型存储服务
type Store[T any] struct { type Store[T any] struct {
option StoreOption option StoreOption
data T data atomic.Value // stores T
dataMap map[string]any dataMap sync.Map // thread-safe map
unsaved bool unsaved atomic.Bool
lock sync.RWMutex initOnce sync.Once
logger *log.LoggerService logger *log.LoggerService
} }
@@ -35,127 +35,152 @@ func NewStore[T any](option StoreOption) *Store[T] {
logger = log.New() 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]{ store := &Store[T]{
option: option, option: option,
dataMap: make(map[string]any),
logger: logger, logger: logger,
} }
// 加载数据 // 异步初始化
if err := store.load(); err != nil { store.initOnce.Do(func() {
logger.Error("store: Failed to load data", "error", err) store.initialize()
} })
return store return store
} }
// 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
}
}
// 加载数据
s.load()
}
// load 加载数据 // load 加载数据
func (s *Store[T]) load() error { func (s *Store[T]) load() {
if s.option.FilePath == "" { if s.option.FilePath == "" {
return fmt.Errorf("store: FilePath not set") return
} }
// 如果文件不存在 // 检查文件是否存在
if _, err := os.Stat(s.option.FilePath); os.IsNotExist(err) { if _, err := os.Stat(s.option.FilePath); os.IsNotExist(err) {
return nil return
} }
file, err := os.Open(s.option.FilePath) data, err := os.ReadFile(s.option.FilePath)
if err != nil { if err != nil {
return fmt.Errorf("store: Failed to open file: %w", err) s.logger.Error("store: failed to read file", "error", err)
} return
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 { if len(data) == 0 {
return nil return
} }
s.lock.Lock() var value T
defer s.lock.Unlock() if err := json.Unmarshal(data, &value); err != nil {
if err := json.Unmarshal(bytes, &s.data); err != nil {
// 尝试加载为map格式 // 尝试加载为map格式
if err := json.Unmarshal(bytes, &s.dataMap); err != nil { var mapData map[string]any
return fmt.Errorf("store: Failed to parse data: %w", err) 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
} }
return nil s.data.Store(value)
} }
// Save 保存数据 // Save 保存数据
func (s *Store[T]) Save() error { func (s *Store[T]) Save() error {
s.lock.Lock() if !s.unsaved.Load() {
defer s.lock.Unlock() return nil // 没有未保存的更改
err := s.saveInternal()
if err != nil {
s.logger.Error("store: Failed to save", "error", err)
return fmt.Errorf("store: Failed to save: %w", err)
} }
s.unsaved = false if err := s.saveInternal(); err != nil {
return fmt.Errorf("store: failed to save: %w", err)
}
s.unsaved.Store(false)
return nil return nil
} }
// saveInternal 内部保存实现 // saveInternal 内部保存实现
func (s *Store[T]) saveInternal() error { func (s *Store[T]) saveInternal() error {
if s.option.FilePath == "" { 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) dir := filepath.Dir(s.option.FilePath)
// 创建临时文件
tempFile, err := os.CreateTemp(dir, "store-*.tmp") tempFile, err := os.CreateTemp(dir, "store-*.tmp")
if err != nil { 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() { defer func() {
_ = tempFile.Close() tempFile.Close()
// 如果出错,删除临时文件
if err != nil { if err != nil {
_ = os.Remove(tempFilePath) os.Remove(tempPath)
} }
}() }()
// 序列化数据 // 写入数据并同步
bytes, err := json.MarshalIndent(s.data, "", " ") if _, err = tempFile.Write(data); err != nil {
if err != nil { return fmt.Errorf("failed to write data: %w", err)
return fmt.Errorf("store: Failed to serialize 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 { 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 { 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 { if err = os.Rename(tempPath, s.option.FilePath); err != nil {
return fmt.Errorf("store: Failed to rename file: %w", err) return fmt.Errorf("failed to rename file: %w", err)
} }
return nil return nil
@@ -163,70 +188,67 @@ func (s *Store[T]) saveInternal() error {
// Get 获取数据 // Get 获取数据
func (s *Store[T]) Get() T { func (s *Store[T]) Get() T {
s.lock.RLock() if value := s.data.Load(); value != nil {
defer s.lock.RUnlock() return value.(T)
return s.data }
var zero T
return zero
} }
// GetProperty 获取指定属性 // GetProperty 获取指定属性
func (s *Store[T]) GetProperty(key string) any { func (s *Store[T]) GetProperty(key string) any {
s.lock.RLock()
defer s.lock.RUnlock()
if key == "" { 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 s.dataMap[key] return true
})
return result
}
if value, ok := s.dataMap.Load(key); ok {
return value
}
return nil
} }
// Set 设置数据 // Set 设置数据
func (s *Store[T]) Set(data T) error { func (s *Store[T]) Set(data T) error {
s.lock.Lock() s.data.Store(data)
defer s.lock.Unlock() s.unsaved.Store(true)
s.data = data
s.unsaved = true
if s.option.AutoSave { if s.option.AutoSave {
return s.saveInternal() return s.saveInternal()
} }
return nil return nil
} }
// SetProperty 设置指定属性 // SetProperty 设置指定属性
func (s *Store[T]) SetProperty(key string, value any) error { func (s *Store[T]) SetProperty(key string, value any) error {
s.lock.Lock() s.dataMap.Store(key, value)
defer s.lock.Unlock() s.unsaved.Store(true)
s.dataMap[key] = value
s.unsaved = true
if s.option.AutoSave { if s.option.AutoSave {
return s.saveInternal() return s.saveInternal()
} }
return nil return nil
} }
// Delete 删除指定属性 // Delete 删除指定属性
func (s *Store[T]) Delete(key string) error { func (s *Store[T]) Delete(key string) error {
s.lock.Lock() s.dataMap.Delete(key)
defer s.lock.Unlock() s.unsaved.Store(true)
delete(s.dataMap, key)
s.unsaved = true
if s.option.AutoSave { if s.option.AutoSave {
return s.saveInternal() return s.saveInternal()
} }
return nil return nil
} }
// HasUnsavedChanges 是否有未保存的更改 // HasUnsavedChanges 是否有未保存的更改
func (s *Store[T]) HasUnsavedChanges() bool { func (s *Store[T]) HasUnsavedChanges() bool {
s.lock.RLock() return s.unsaved.Load()
defer s.lock.RUnlock()
return s.unsaved
} }

View File

@@ -31,7 +31,6 @@ func (ts *TrayService) SetAppReferences(app *application.App, mainWindow *applic
func (ts *TrayService) ShouldMinimizeToTray() bool { func (ts *TrayService) ShouldMinimizeToTray() bool {
config, err := ts.configService.GetConfig() config, err := ts.configService.GetConfig()
if err != nil { if err != nil {
ts.logger.Error("TrayService: Failed to get config", "error", err)
return true // 默认行为:隐藏到托盘 return true // 默认行为:隐藏到托盘
} }
@@ -44,11 +43,9 @@ func (ts *TrayService) HandleWindowClose() {
// 隐藏到托盘 // 隐藏到托盘
ts.mainWindow.Hide() ts.mainWindow.Hide()
ts.app.EmitEvent("window:hidden", nil) ts.app.EmitEvent("window:hidden", nil)
ts.logger.Info("TrayService: Window hidden to system tray")
} else { } else {
// 直接退出应用 // 直接退出应用
ts.app.Quit() ts.app.Quit()
ts.logger.Info("TrayService: Application quit")
} }
} }
@@ -58,10 +55,6 @@ func (ts *TrayService) HandleWindowMinimize() {
// 隐藏到托盘 // 隐藏到托盘
ts.mainWindow.Hide() ts.mainWindow.Hide()
ts.app.EmitEvent("window:hidden", nil) 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 { if ts.app != nil {
ts.app.EmitEvent("window:shown", 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() { func (ts *TrayService) MinimizeButtonClicked() {
// 最小化按钮总是执行正常最小化到任务栏,不隐藏到托盘 // 最小化按钮总是执行正常最小化到任务栏,不隐藏到托盘
ts.mainWindow.Minimise() ts.mainWindow.Minimise()
ts.logger.Info("TrayService: Window minimized to taskbar via titlebar button")
} }

View File

@@ -73,7 +73,6 @@ func main() {
// 'Mac' options tailor the window when running on macOS. // 'Mac' options tailor the window when running on macOS.
// 'BackgroundColour' is the background colour of the window. // 'BackgroundColour' is the background colour of the window.
// 'URL' is the URL that will be loaded into the webview. // 'URL' is the URL that will be loaded into the webview.
log.Println("Creating main window...")
mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "voidraft", Title: "voidraft",
Width: 700, Width: 700,
@@ -106,7 +105,6 @@ func main() {
hotkeyService := serviceManager.GetHotkeyService() hotkeyService := serviceManager.GetHotkeyService()
err := hotkeyService.Initialize(app) err := hotkeyService.Initialize(app)
if err != nil { if err != nil {
log.Printf("Failed to initialize hotkey service: %v\n", err)
panic(err) panic(err)
} }