chore: import Local-Cursor v1.1.8 source snapshot
Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage. Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
This commit is contained in:
2
internal/app/doc.go
Normal file
2
internal/app/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package app 负责组装桌面应用入口、窗口、托盘和 Wails 服务。
|
||||
package app
|
||||
25
internal/app/quit_dialog.go
Normal file
25
internal/app/quit_dialog.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// 退出联动确认对话框:Cursor 未退出时询问用户是否同步关闭。
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// showQuitConfirmDialog 显示确认对话框,返回用户是否确认退出并关闭 Cursor。
|
||||
// 注意:Wails Windows 实现按 MessageBox 返回值(Yes/No)匹配按钮标签,
|
||||
// 因此按钮标签必须使用 "Yes"/"No" 才能触发回调。
|
||||
func showQuitConfirmDialog(app *application.App) bool {
|
||||
confirmed := false
|
||||
dialog := app.Dialog.Question()
|
||||
dialog.SetTitle("退出 Local-Cursor")
|
||||
dialog.SetMessage("检测到 Cursor 正在运行。\n退出 Local-Cursor 将同步关闭所有 Cursor 进程,是否确定?")
|
||||
yesButton := dialog.AddButton("Yes").OnClick(func() {
|
||||
confirmed = true
|
||||
})
|
||||
yesButton.SetAsDefault()
|
||||
noButton := dialog.AddButton("No")
|
||||
noButton.SetAsCancel()
|
||||
dialog.Show()
|
||||
// Windows 实现中 MessageBox 为模态调用,Show() 返回时回调已执行。
|
||||
return confirmed
|
||||
}
|
||||
271
internal/app/runner.go
Normal file
271
internal/app/runner.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"io/fs"
|
||||
goruntime "runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
serverconfig "cursor/internal/backend/server/config"
|
||||
"cursor/internal/buildinfo"
|
||||
|
||||
"github.com/leaanthony/u"
|
||||
|
||||
bridge "cursor/internal/bridge"
|
||||
"cursor/internal/certs"
|
||||
"cursor/internal/cursor"
|
||||
"cursor/internal/logger"
|
||||
"cursor/internal/mitm"
|
||||
"cursor/internal/netproxy"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "Local-Cursor"
|
||||
)
|
||||
|
||||
// EmbeddedResources 定义了当前模块中的 EmbeddedResources 类型。
|
||||
type EmbeddedResources struct {
|
||||
// Assets 表示当前声明中的 Assets。
|
||||
Assets fs.FS
|
||||
// AppIcon 表示当前声明中的 AppIcon。
|
||||
AppIcon []byte
|
||||
// TrayIcon 表示当前声明中的 TrayIcon。
|
||||
TrayIcon []byte
|
||||
}
|
||||
|
||||
// init 用于处理与 init 相关的逻辑。
|
||||
func init() {
|
||||
application.RegisterEvent[bridge.ProxyState]("proxy:state")
|
||||
application.RegisterEvent[bridge.UserConfig]("user-config:changed")
|
||||
application.RegisterEvent[bridge.ModelAdapterTestResultsPayload]("model-adapter-test:updated")
|
||||
}
|
||||
|
||||
// Run 用于处理与 Run 相关的逻辑。
|
||||
func Run(resources EmbeddedResources) error {
|
||||
logger.Init()
|
||||
ensureStartMenuShortcut()
|
||||
netproxy.InstallDefaultTransport()
|
||||
|
||||
embeddedCACertPEM := certs.EmbeddedCACertPEM()
|
||||
logEmbeddedCAInfo(embeddedCACertPEM)
|
||||
|
||||
certManager, err := certs.NewEmbeddedManager()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultBackendBaseURL := "http://" + serverconfig.DefaultBackendListenAddr
|
||||
proxyServer, err := mitm.NewProxyServer(serverconfig.DefaultProxyListenAddr, defaultBackendBaseURL, "", "", certManager)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proxyService := bridge.NewProxyService(proxyServer, certManager, embeddedCACertPEM)
|
||||
metricsService := bridge.NewMetricsService()
|
||||
windowService := bridge.NewWindowService()
|
||||
skillService := bridge.NewSkillService()
|
||||
app := application.New(application.Options{
|
||||
Name: appName,
|
||||
Description: appName,
|
||||
Services: []application.Service{
|
||||
application.NewService(proxyService),
|
||||
application.NewService(metricsService),
|
||||
application.NewService(windowService),
|
||||
application.NewService(skillService),
|
||||
},
|
||||
Assets: application.AssetOptions{
|
||||
Handler: application.AssetFileServerFS(resources.Assets),
|
||||
},
|
||||
Mac: application.MacOptions{
|
||||
ActivationPolicy: application.ActivationPolicyAccessory,
|
||||
ApplicationShouldTerminateAfterLastWindowClosed: false,
|
||||
},
|
||||
OnShutdown: func() {
|
||||
proxyService.ShutdownForQuit()
|
||||
},
|
||||
SingleInstance: &application.SingleInstanceOptions{
|
||||
UniqueID: "com.local-curosr.single-instance",
|
||||
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
||||
logger.Infof("检测到实例请求,已忽略")
|
||||
// 不激活窗口,避免干扰用户工作
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
windowService.SetApp(app)
|
||||
|
||||
mainWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: appName,
|
||||
Width: 760,
|
||||
Height: 660,
|
||||
MinWidth: 680,
|
||||
MinHeight: 560,
|
||||
DisableResize: false,
|
||||
Frameless: goruntime.GOOS == "windows",
|
||||
URL: "/",
|
||||
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.True,
|
||||
TextInteractionEnabled: u.True,
|
||||
AllowsBackForwardNavigationGestures: u.False,
|
||||
},
|
||||
},
|
||||
Windows: application.WindowsWindow{
|
||||
HiddenOnTaskbar: false,
|
||||
},
|
||||
})
|
||||
|
||||
window := mainWindow
|
||||
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
||||
window.Hide()
|
||||
e.Cancel()
|
||||
})
|
||||
|
||||
showMainWindow := func() {
|
||||
window.Show().Focus()
|
||||
}
|
||||
toggleMainWindow := func() {
|
||||
if window.IsVisible() {
|
||||
window.Hide()
|
||||
return
|
||||
}
|
||||
showMainWindow()
|
||||
}
|
||||
|
||||
systray := app.SystemTray.New()
|
||||
menu := app.Menu.New()
|
||||
statusItem := menu.Add("状态:未启动").SetEnabled(false)
|
||||
menu.AddSeparator()
|
||||
startItem := menu.Add("启动服务")
|
||||
stopItem := menu.Add("停止服务")
|
||||
menu.AddSeparator()
|
||||
menu.Add("显示窗口").OnClick(func(ctx *application.Context) {
|
||||
showMainWindow()
|
||||
})
|
||||
menu.Add("隐藏窗口").OnClick(func(ctx *application.Context) {
|
||||
window.Hide()
|
||||
})
|
||||
menu.AddSeparator()
|
||||
menu.Add("退出").OnClick(func(ctx *application.Context) {
|
||||
// 与 Cursor 进程联动:Cursor 未退出时先询问,确认后同步关闭 Cursor。
|
||||
if cursor.IsCursorRunning() {
|
||||
confirmed := showQuitConfirmDialog(app)
|
||||
if !confirmed {
|
||||
return
|
||||
}
|
||||
if err := cursor.KillCursorProcesses(); err != nil {
|
||||
logger.Errorf("quit: kill cursor processes failed err=%v", err)
|
||||
}
|
||||
}
|
||||
proxyService.ShutdownForQuit()
|
||||
app.Quit()
|
||||
})
|
||||
|
||||
refreshTray := func() {
|
||||
state := proxyService.GetState()
|
||||
if state.Running {
|
||||
statusItem.SetLabel("状态:运行中")
|
||||
startItem.SetEnabled(false)
|
||||
stopItem.SetEnabled(true)
|
||||
} else {
|
||||
statusItem.SetLabel("状态:未启动")
|
||||
startItem.SetEnabled(true)
|
||||
stopItem.SetEnabled(false)
|
||||
}
|
||||
}
|
||||
app.Event.On("proxy:state", func(event *application.CustomEvent) {
|
||||
refreshTray()
|
||||
})
|
||||
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
|
||||
logger.Infof("应用版本:v%s", buildinfo.CurrentVersion())
|
||||
go func() {
|
||||
logger.Infof("application started, begin auto start service in background")
|
||||
if _, err := proxyService.StartProxy(); err != nil {
|
||||
logger.Errorf("自动启动服务失败: %v", err)
|
||||
} else {
|
||||
state := proxyService.GetState()
|
||||
logger.Infof("代理已自动启动: %s", state.ProxyListenAddr)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
startItem.OnClick(func(ctx *application.Context) {
|
||||
if _, err := proxyService.StartProxy(); err != nil {
|
||||
logger.Errorf("启动服务失败: %v", err)
|
||||
}
|
||||
refreshTray()
|
||||
})
|
||||
stopItem.OnClick(func(ctx *application.Context) {
|
||||
if _, err := proxyService.StopProxy(); err != nil {
|
||||
logger.Errorf("停止服务失败: %v", err)
|
||||
}
|
||||
refreshTray()
|
||||
})
|
||||
|
||||
if len(resources.AppIcon) > 0 {
|
||||
switch goruntime.GOOS {
|
||||
case "darwin":
|
||||
systray.SetTemplateIcon(resources.TrayIcon)
|
||||
case "windows":
|
||||
systray.SetIcon(resources.AppIcon)
|
||||
default:
|
||||
systray.SetIcon(resources.TrayIcon)
|
||||
}
|
||||
}
|
||||
systray.SetTooltip(appName)
|
||||
systray.OnClick(toggleMainWindow).SetMenu(menu)
|
||||
refreshTray()
|
||||
|
||||
return app.Run()
|
||||
}
|
||||
|
||||
// logEmbeddedCAInfo 用于处理与 logEmbeddedCAInfo 相关的逻辑。
|
||||
func logEmbeddedCAInfo(certPEM []byte) {
|
||||
if len(certPEM) == 0 {
|
||||
logger.Errorf("embedded CA is empty")
|
||||
return
|
||||
}
|
||||
cert, err := parseEmbeddedCert(certPEM)
|
||||
if err != nil {
|
||||
logger.Errorf("parse embedded CA failed: %v", err)
|
||||
return
|
||||
}
|
||||
sum := sha256.Sum256(cert.Raw)
|
||||
logger.Infof(
|
||||
"embedded CA loaded: sha256=%s subject=%s valid=%s~%s",
|
||||
strings.ToUpper(hex.EncodeToString(sum[:])),
|
||||
cert.Subject.String(),
|
||||
cert.NotBefore.Format(time.RFC3339),
|
||||
cert.NotAfter.Format(time.RFC3339),
|
||||
)
|
||||
}
|
||||
|
||||
// parseEmbeddedCert 用于处理与 parseEmbeddedCert 相关的逻辑。
|
||||
func parseEmbeddedCert(data []byte) (*x509.Certificate, error) {
|
||||
if block, _ := pem.Decode(data); block != nil {
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
return x509.ParseCertificate(data)
|
||||
}
|
||||
6
internal/app/shortcut_other.go
Normal file
6
internal/app/shortcut_other.go
Normal file
@@ -0,0 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package app
|
||||
|
||||
// ensureStartMenuShortcut 非 Windows 平台不做开始菜单注册。
|
||||
func ensureStartMenuShortcut() {}
|
||||
48
internal/app/shortcut_windows.go
Normal file
48
internal/app/shortcut_windows.go
Normal file
@@ -0,0 +1,48 @@
|
||||
//go:build windows
|
||||
|
||||
// 快捷方式注册:把本体写入开始菜单,使 Flow Launcher、PowerToys Run、
|
||||
// Windows 搜索等工具能索引并快速启动。程序运行时幂等创建,不随退出删除。
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"cursor/internal/logger"
|
||||
"cursor/internal/winexec"
|
||||
)
|
||||
|
||||
// startMenuShortcutPath 返回开始菜单快捷方式的完整路径。
|
||||
func startMenuShortcutPath() string {
|
||||
appData := strings.TrimSpace(os.Getenv("APPDATA"))
|
||||
if appData == "" {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
appData = filepath.Join(homeDir, "AppData", "Roaming")
|
||||
}
|
||||
return filepath.Join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Local-Cursor.lnk")
|
||||
}
|
||||
|
||||
// ensureStartMenuShortcut 在开始菜单创建指向当前可执行文件的快捷方式。
|
||||
// 已存在且指向一致时跳过;失败仅记录不阻断启动。
|
||||
func ensureStartMenuShortcut() {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
exePath = filepath.Clean(exePath)
|
||||
lnkPath := startMenuShortcutPath()
|
||||
|
||||
if raw, err := os.ReadFile(lnkPath); err == nil && strings.Contains(strings.ToLower(string(raw)), strings.ToLower(exePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
workDir := filepath.Dir(exePath)
|
||||
psScript := `$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('` + lnkPath + `'); $s.TargetPath = '` + exePath + `'; $s.WorkingDirectory = '` + workDir + `'; $s.Description = 'Local-Cursor'; $s.Save()`
|
||||
cmd := winexec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript)
|
||||
if err := cmd.Run(); err != nil {
|
||||
logger.Infof("ensureStartMenuShortcut: create failed lnk=%s err=%v", lnkPath, err)
|
||||
return
|
||||
}
|
||||
logger.Infof("ensureStartMenuShortcut: created lnk=%s target=%s", lnkPath, exePath)
|
||||
}
|
||||
Reference in New Issue
Block a user