🎨 Optimize code
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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 取消注册全局热键
|
||||||
*/
|
*/
|
||||||
|
@@ -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);
|
|
||||||
|
@@ -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",
|
||||||
};
|
};
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
@@ -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),
|
||||||
},
|
},
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
62
internal/services/path_manager.go
Normal file
62
internal/services/path_manager.go
Normal 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"
|
||||||
|
}
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
|
||||||
}
|
}
|
||||||
|
@@ -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")
|
|
||||||
}
|
}
|
||||||
|
2
main.go
2
main.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user