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:
221
internal/cursor/localize_windows.go
Normal file
221
internal/cursor/localize_windows.go
Normal file
@@ -0,0 +1,221 @@
|
||||
//go:build windows
|
||||
|
||||
// Windows 版 Node 环境探测:GUI 进程往往没有继承 shell 注入的 Node 路径
|
||||
// (fnm/nvm 等按 shell 修改 PATH),因此合并注册表 PATH 与常见安装目录。
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"cursor/internal/winexec"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// mergeNodeSearchPath 合并进程 PATH、注册表用户/系统 PATH 与常见 Node 安装目录。
|
||||
// 返回合并后的 PATH,以及是否发现了额外的候选目录。
|
||||
func mergeNodeSearchPath() (string, bool) {
|
||||
parts := filepath.SplitList(os.Getenv("PATH"))
|
||||
seen := make(map[string]struct{}, len(parts))
|
||||
for _, p := range parts {
|
||||
seen[strings.ToLower(strings.TrimSpace(p))] = struct{}{}
|
||||
}
|
||||
found := false
|
||||
add := func(dir string) {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
return
|
||||
}
|
||||
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
|
||||
return
|
||||
}
|
||||
key := strings.ToLower(dir)
|
||||
if _, exists := seen[key]; exists {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
parts = append(parts, dir)
|
||||
found = true
|
||||
}
|
||||
|
||||
// 注册表中的用户与系统 PATH(GUI 进程可能未刷新)。
|
||||
for _, source := range []struct {
|
||||
root registry.Key
|
||||
path string
|
||||
}{
|
||||
{registry.CURRENT_USER, `Environment`},
|
||||
{registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`},
|
||||
} {
|
||||
value, err := registryExpandString(source.root, source.path, "Path")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, dir := range filepath.SplitList(value) {
|
||||
add(dir)
|
||||
probeFnmDataDirs(dir, add)
|
||||
}
|
||||
}
|
||||
|
||||
// 常见 Node 安装目录兜底(fnm/nvm/volta/scoop/官方安装器)。
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
localAppData := os.Getenv("LOCALAPPDATA")
|
||||
fnmDir := os.Getenv("FNM_DIR")
|
||||
candidates := []string{
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "nodejs"),
|
||||
filepath.Join(localAppData, "Programs", "nodejs"),
|
||||
filepath.Join(homeDir, "AppData", "Roaming", "nvm"),
|
||||
filepath.Join(homeDir, "scoop", "apps", "nodejs", "current"),
|
||||
filepath.Join(homeDir, ".volta", "bin"),
|
||||
}
|
||||
if localAppData != "" {
|
||||
base := fnmDir
|
||||
if base == "" {
|
||||
base = filepath.Join(localAppData, "fnm")
|
||||
}
|
||||
// fnm:aliases/default 指向默认版本;node-versions 下枚举各版本。
|
||||
candidates = append(candidates,
|
||||
filepath.Join(base, "aliases", "default"),
|
||||
filepath.Join(base, "node-versions"),
|
||||
)
|
||||
}
|
||||
for _, dir := range candidates {
|
||||
add(dir)
|
||||
probeFnmDataDirs(dir, add)
|
||||
if strings.EqualFold(filepath.Base(dir), "node-versions") {
|
||||
if versions, err := os.ReadDir(dir); err == nil {
|
||||
for _, version := range versions {
|
||||
add(filepath.Join(dir, version.Name(), "installation"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, string(os.PathListSeparator)), found
|
||||
}
|
||||
|
||||
// probeFnmDataDirs 在 PATH 候选目录中发现 fnm.exe 时,追加探测其数据目录下的
|
||||
// node 版本(fnm 的 FNM_DIR 是 shell 级环境变量,GUI 进程通常拿不到)。
|
||||
func probeFnmDataDirs(dir string, add func(string)) {
|
||||
if _, err := os.Stat(filepath.Join(dir, "fnm.exe")); err != nil {
|
||||
return
|
||||
}
|
||||
// 兼容两种布局:<root>\bin + <root>\data,或 <root>\aliases + <root>\node-versions。
|
||||
for _, dataDir := range []string{
|
||||
filepath.Join(filepath.Dir(dir), "data"),
|
||||
dir,
|
||||
} {
|
||||
add(filepath.Join(dataDir, "aliases", "default"))
|
||||
versionsDir := filepath.Join(dataDir, "node-versions")
|
||||
if versions, err := os.ReadDir(versionsDir); err == nil {
|
||||
for _, version := range versions {
|
||||
add(filepath.Join(versionsDir, version.Name(), "installation"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// registryExpandString 读取注册表字符串值并展开 %VAR% 引用。
|
||||
func registryExpandString(root registry.Key, keyPath string, valueName string) (string, error) {
|
||||
key, err := registry.OpenKey(root, keyPath, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer key.Close()
|
||||
value, _, err := key.GetStringValue(valueName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return os.ExpandEnv(value), nil
|
||||
}
|
||||
|
||||
// detectCursorInstallPathFromRegistry 从 Windows 卸载项中查找 Cursor 安装位置。
|
||||
func detectCursorInstallPathFromRegistry() string {
|
||||
for _, source := range []struct {
|
||||
root registry.Key
|
||||
path string
|
||||
}{
|
||||
{registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
{registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
{registry.LOCAL_MACHINE, `Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
} {
|
||||
root, err := registry.OpenKey(source.root, source.path, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
names, err := root.ReadSubKeyNames(-1)
|
||||
root.Close()
|
||||
if err != nil {
|
||||
// 部分安全软件会拦截 Go 进程的注册表枚举,降级用 reg.exe 查询子键名。
|
||||
names = regQuerySubKeyNames(registryKeyPath(source.root, source.path))
|
||||
}
|
||||
for _, name := range names {
|
||||
sub, err := registry.OpenKey(source.root, source.path+`\`+name, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 安装器(如 Inno Setup)常用 GUID 作键名,DisplayName 在值里;
|
||||
// 不含 cursor 的条目仅在 InstallLocation 指向有效 Cursor 目录时采用。
|
||||
dir := readCursorInstallFromKey(sub)
|
||||
sub.Close()
|
||||
if dir != "" {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// registryKeyPath 返回 reg.exe 可识别的根键路径表示。
|
||||
func registryKeyPath(root registry.Key, path string) string {
|
||||
switch root {
|
||||
case registry.CURRENT_USER:
|
||||
return `HKCU\` + path
|
||||
case registry.LOCAL_MACHINE:
|
||||
return `HKLM\` + path
|
||||
default:
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// regQuerySubKeyNames 通过 reg.exe 枚举子键名,作为 Go 注册表枚举被拦截时的降级方案。
|
||||
func regQuerySubKeyNames(fullPath string) []string {
|
||||
out, err := winexec.Command("reg", "query", fullPath).Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
prefix := strings.ToUpper(fullPath) + `\`
|
||||
names := make([]string, 0, 16)
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(strings.ToUpper(line), prefix) {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(line[len(prefix):])
|
||||
if name != "" && !strings.Contains(name, " ") {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// readCursorInstallFromKey 从单个卸载项读取并校验安装目录。
|
||||
func readCursorInstallFromKey(key registry.Key) string {
|
||||
candidates := make([]string, 0, 2)
|
||||
if value, _, err := key.GetStringValue("InstallLocation"); err == nil {
|
||||
candidates = append(candidates, value)
|
||||
}
|
||||
if value, _, err := key.GetStringValue("DisplayIcon"); err == nil {
|
||||
// DisplayIcon 形如 "D:\path\Cursor.exe" 或带 ",0" 后缀。
|
||||
icon := strings.SplitN(value, ",", 2)[0]
|
||||
icon = strings.Trim(icon, `"`)
|
||||
candidates = append(candidates, filepath.Dir(icon))
|
||||
}
|
||||
for _, dir := range candidates {
|
||||
if isCursorInstallDir(strings.TrimSpace(dir)) {
|
||||
return filepath.Clean(strings.TrimSpace(dir))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user