package bridge import ( "cursor/internal/buildinfo" "cursor/internal/client" "cursor/internal/cursor" "fmt" "os" "os/exec" goruntime "runtime" "sync" "github.com/leaanthony/u" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/events" ) // modelEditorContext 保存当前模型编辑器窗口的初始化上下文。 type modelEditorContext struct { Index int `json:"index"` AdapterJSON string `json:"adapterJSON"` } // WindowService 定义了当前模块中的 WindowService 类型。 type WindowService struct { app *application.App modelConfigWindow *application.WebviewWindow modelEditorWindow *application.WebviewWindow skillWindow *application.WebviewWindow editorCtx *modelEditorContext mu sync.RWMutex } // NewWindowService 用于处理与 NewWindowService 相关的逻辑。 func NewWindowService() *WindowService { return &WindowService{} } // SetApp 用于处理与 SetApp 相关的逻辑。 func (s *WindowService) SetApp(app *application.App) { s.mu.Lock() defer s.mu.Unlock() s.app = app } // GetAppVersion 返回当前应用版本号。 func (s *WindowService) GetAppVersion() string { return buildinfo.CurrentVersion() } // OpenConfigWindow 打开本地设置目录。 func (s *WindowService) OpenConfigWindow() { logServiceCall("window", "open_config_directory") if err := os.MkdirAll(client.ResolveSettingsRootPath(), 0o755); err != nil { logServiceResult("window", "open_config_directory", err) return } logServiceResult("window", "open_config_directory", openDirectory(client.ResolveSettingsRootPath())) } // OpenModelConfigWindow 打开模型配置独立窗口。如果窗口已存在则聚焦。 func (s *WindowService) OpenModelConfigWindow() { logServiceCall("window", "open_model_config") s.mu.Lock() defer s.mu.Unlock() if s.app == nil { logServiceResult("window", "open_model_config", errServiceNotReady) return } if s.modelConfigWindow != nil { s.modelConfigWindow.Show() s.modelConfigWindow.Focus() logServiceResult("window", "open_model_config", nil) return } win := s.app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "模型配置", Width: 980, Height: 700, MinWidth: 820, MinHeight: 560, DisableResize: false, Frameless: goruntime.GOOS == "windows", URL: "/#/model-config", Hidden: false, HideOnEscape: false, MinimiseButtonState: application.ButtonEnabled, MaximiseButtonState: application.ButtonEnabled, CloseButtonState: application.ButtonEnabled, BackgroundColour: application.RGBA{Red: 25, Green: 25, Blue: 25, Alpha: 255}, Mac: application.MacWindow{ Backdrop: application.MacBackdropLiquidGlass, DisableShadow: false, TitleBar: application.MacTitleBar{ AppearsTransparent: true, Hide: false, HideTitle: true, FullSizeContent: true, UseToolbar: false, HideToolbarSeparator: true, }, WebviewPreferences: application.MacWebviewPreferences{ FullscreenEnabled: u.True, TextInteractionEnabled: u.True, AllowsBackForwardNavigationGestures: u.False, }, }, Windows: application.WindowsWindow{ HiddenOnTaskbar: false, }, }) win.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { s.mu.Lock() defer s.mu.Unlock() s.modelConfigWindow = nil }) s.modelConfigWindow = win logServiceResult("window", "open_model_config", nil) } // OpenModelEditorWindow 打开模型编辑器独立窗口。 // index < 0 表示新增,>= 0 表示编辑对应索引的适配器。 // adapterJSON 为编辑器初始数据的 JSON 字符串。 func (s *WindowService) OpenModelEditorWindow(index int, adapterJSON string) { logServiceCall("window", "open_model_editor", "index", index) s.mu.Lock() defer s.mu.Unlock() if s.app == nil { logServiceResult("window", "open_model_editor", errServiceNotReady) return } s.editorCtx = &modelEditorContext{ Index: index, AdapterJSON: adapterJSON, } if s.modelEditorWindow != nil { s.modelEditorWindow.Show() s.modelEditorWindow.Focus() logServiceResult("window", "open_model_editor", nil) return } title := "新增模型配置" if index >= 0 { title = "编辑模型配置" } win := s.app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: title, Width: 840, Height: 680, MinWidth: 740, MinHeight: 600, DisableResize: false, Frameless: goruntime.GOOS == "windows", URL: fmt.Sprintf("/#/model-editor?index=%d", index), Hidden: false, HideOnEscape: false, MinimiseButtonState: application.ButtonEnabled, MaximiseButtonState: application.ButtonHidden, CloseButtonState: application.ButtonEnabled, BackgroundColour: application.RGBA{Red: 25, Green: 25, Blue: 25, Alpha: 255}, Mac: application.MacWindow{ Backdrop: application.MacBackdropLiquidGlass, DisableShadow: false, TitleBar: application.MacTitleBar{ AppearsTransparent: true, Hide: false, HideTitle: true, FullSizeContent: true, UseToolbar: false, HideToolbarSeparator: true, }, WebviewPreferences: application.MacWebviewPreferences{ FullscreenEnabled: u.False, TextInteractionEnabled: u.True, AllowsBackForwardNavigationGestures: u.False, }, }, Windows: application.WindowsWindow{ HiddenOnTaskbar: false, }, }) win.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { s.mu.Lock() defer s.mu.Unlock() s.modelEditorWindow = nil s.editorCtx = nil }) s.modelEditorWindow = win logServiceResult("window", "open_model_editor", nil) } // GetModelEditorContext 返回当前编辑器窗口的初始化上下文。 func (s *WindowService) GetModelEditorContext() map[string]any { s.mu.RLock() defer s.mu.RUnlock() if s.editorCtx == nil { return map[string]any{ "index": -1, "adapterJSON": "{}", } } return map[string]any{ "index": s.editorCtx.Index, "adapterJSON": s.editorCtx.AdapterJSON, } } // CursorStatus 描述 Cursor IDE 的连接状态。 type CursorStatus struct { InstallFound bool `json:"installFound"` InstallPath string `json:"installPath"` ProcessRunning bool `json:"processRunning"` } // GetCursorStatus 返回 Cursor 安装目录探测结果与进程运行状态。 func (s *WindowService) GetCursorStatus() CursorStatus { path := cursor.DetectCursorInstallPath() return CursorStatus{ InstallFound: path != "", InstallPath: path, ProcessRunning: cursor.IsCursorRunning(), } } // LocalizeCursor 关闭 Cursor 进程并运行社区汉化包。 // installDir 为空时自动检测 Cursor 安装目录。 func (s *WindowService) LocalizeCursor(installDir string) (string, error) { logServiceCall("cursor", "localize") output, err := cursor.LocalizeCursor(installDir) logServiceResult("cursor", "localize", err) return output, err } // OpenCursor 启动 Cursor IDE(installDir 为空时自动检测)。 func (s *WindowService) OpenCursor(installDir string) error { logServiceCall("cursor", "open", "install_dir", installDir) err := cursor.OpenCursor(installDir) logServiceResult("cursor", "open", err) return err } // SelectDirectory 弹出系统目录选择对话框,返回所选目录;取消时返回空字符串。 func (s *WindowService) SelectDirectory() (string, error) { logServiceCall("window", "select_cursor_directory") if s == nil || s.app == nil { err := errServiceNotReady logServiceResult("window", "select_cursor_directory", err) return "", err } path, err := s.app.Dialog.OpenFile(). CanChooseDirectories(true). CanChooseFiles(false). SetTitle("选择 Cursor 安装目录"). PromptForSingleSelection() logServiceResult("window", "select_cursor_directory", err) return path, err } // OpenSkillWindow 打开 Skill 管理子窗口。如果窗口已存在则聚焦。 func (s *WindowService) OpenSkillWindow() { logServiceCall("window", "open_skills") s.mu.Lock() defer s.mu.Unlock() if s.app == nil { logServiceResult("window", "open_skills", errServiceNotReady) return } if s.skillWindow != nil { s.skillWindow.Show() s.skillWindow.Focus() logServiceResult("window", "open_skills", nil) return } win := s.app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Skill 管理", Width: 720, Height: 640, MinWidth: 620, MinHeight: 520, DisableResize: false, Frameless: goruntime.GOOS == "windows", URL: "/#/skills", Hidden: false, HideOnEscape: false, MinimiseButtonState: application.ButtonEnabled, MaximiseButtonState: application.ButtonEnabled, CloseButtonState: application.ButtonEnabled, BackgroundColour: application.RGBA{Red: 25, Green: 25, Blue: 25, Alpha: 255}, Windows: application.WindowsWindow{ HiddenOnTaskbar: false, }, }) win.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { s.mu.Lock() defer s.mu.Unlock() s.skillWindow = nil }) s.skillWindow = win logServiceResult("window", "open_skills", nil) } // OpenHistoryWindow 打开日志目录。 func (s *WindowService) OpenHistoryWindow() { logServiceCall("window", "open_logs_directory") if err := os.MkdirAll(client.ResolveLogsRootPath(), 0o755); err != nil { logServiceResult("window", "open_logs_directory", err) return } logServiceResult("window", "open_logs_directory", openDirectory(client.ResolveLogsRootPath())) } // openDirectory 使用系统默认文件管理器打开目录并返回启动错误。 func openDirectory(path string) error { if path == "" { return nil } var err error switch goruntime.GOOS { case "darwin": err = exec.Command("open", path).Start() case "windows": err = exec.Command("explorer", path).Start() default: err = exec.Command("xdg-open", path).Start() } return err }