//go:build windows package apps import ( "fmt" "os" "os/exec" "path/filepath" "strings" "syscall" "golang.org/x/sys/windows/registry" ) // ScanApps returns a list of installed applications via Registry func ScanApps() ([]AppInfo, error) { var apps []AppInfo // Keys to search // HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall // HKLM Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall // HKCU Software\Microsoft\Windows\CurrentVersion\Uninstall keys := []struct { hive registry.Key path string }{ {registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`}, {registry.LOCAL_MACHINE, `Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`}, {registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Uninstall`}, } seen := make(map[string]bool) for _, k := range keys { baseKey, err := registry.OpenKey(k.hive, k.path, registry.READ) if err != nil { continue } subkeys, err := baseKey.ReadSubKeyNames(-1) baseKey.Close() if err != nil { continue } for _, subkeyName := range subkeys { appKey, err := registry.OpenKey(k.hive, k.path+`\`+subkeyName, registry.READ) if err != nil { continue } displayName, _, err := appKey.GetStringValue("DisplayName") if err != nil || displayName == "" { appKey.Close() continue } // Define installLocation explicitly installLocation, _, _ := appKey.GetStringValue("InstallLocation") uninstallString, _, _ := appKey.GetStringValue("UninstallString") quietUninstallString, _, _ := appKey.GetStringValue("QuietUninstallString") if uninstallString == "" && quietUninstallString != "" { uninstallString = quietUninstallString } // Debug Log if strings.Contains(displayName, "Foxit") { fmt.Printf("found Foxit: %s | UninstallString: %s\n", displayName, uninstallString) } // Deduplication: If we've seen this Name + Location combination, skip it. // This handles the common case of 32-bit apps appearing in both HKLM and WOW6432Node. dedupKey := displayName + "|" + strings.ToLower(installLocation) if seen[dedupKey] { appKey.Close() continue } seen[dedupKey] = true // Try to get size from registry (EstimatedSize is in KB) sizeVal, _, errSize := appKey.GetIntegerValue("EstimatedSize") var sizeBytes int64 if errSize == nil { sizeBytes = int64(sizeVal) * 1024 } // Construct Full Registry Key Path as BundleID for later use hiveName := "HKLM" if k.hive == registry.CURRENT_USER { hiveName = "HKCU" } fullRegPath := hiveName + `\` + k.path + `\` + subkeyName apps = append(apps, AppInfo{ Name: displayName, Path: installLocation, BundleID: fullRegPath, UninstallString: uninstallString, Size: sizeBytes, }) appKey.Close() } } return apps, nil } // GetAppDetails finds all associated files (simplified for Windows) func GetAppDetails(appPath, bundleID string) (*AppDetails, error) { // appPath might come from ScanApps which set it to InstallLocation. // bundleID is used as the Registry Key Path. // Re-construct basic info info := AppInfo{ Name: filepath.Base(appPath), Path: appPath, BundleID: bundleID, // UninstallString is hard to recover if not passed, but usually we call GetAppDetails after ScanApps which has it. // For now leave empty, or we'd need to re-query registry if bundleID is a registry path. Size: 0, } if appPath == "" && bundleID != "" { // Fallback name if path is empty parts := strings.Split(bundleID, `\`) if len(parts) > 0 { info.Name = parts[len(parts)-1] } } details := &AppDetails{ AppInfo: info, TotalSize: 0, } // 1. Scan File System if appPath != "" { var size int64 filepath.WalkDir(appPath, func(_ string, d os.DirEntry, err error) error { if err == nil && !d.IsDir() { i, _ := d.Info() size += i.Size() } return nil }) details.AppInfo.Size = size details.TotalSize = size // Add the main folder as associated data details.Associated = append(details.Associated, AssociatedFile{ Path: appPath, Type: "data", Size: size, }) } // 2. Add Registry Key (Uninstall Entry) if bundleID != "" && (strings.HasPrefix(bundleID, "HKLM") || strings.HasPrefix(bundleID, "HKCU")) { // We treat the registry key as a "file" with special type and 0 size details.Associated = append(details.Associated, AssociatedFile{ Path: "REG:" + bundleID, Type: "registry", // New type Size: 0, // Registry entries are negligible in size }) } return details, nil } // DeleteFiles removes the requested paths func DeleteFiles(paths []string) error { for _, p := range paths { if p == "" { continue } // Registry Deletion if strings.HasPrefix(p, "REG:") { regPath := strings.TrimPrefix(p, "REG:") deleteRegistryKey(regPath) continue } // Safety checks if p == "C:\\" || p == "c:\\" || p == "C:\\Windows" || strings.HasPrefix(strings.ToLower(p), "c:\\windows") { continue } err := os.RemoveAll(p) if err != nil { // Log error but continue? Or return? // return err // On Windows file locking is common, best effort } } return nil } func deleteRegistryKey(fullPath string) error { var hive registry.Key var subPath string if strings.HasPrefix(fullPath, "HKLM\\") { hive = registry.LOCAL_MACHINE subPath = strings.TrimPrefix(fullPath, "HKLM\\") } else if strings.HasPrefix(fullPath, "HKCU\\") { hive = registry.CURRENT_USER subPath = strings.TrimPrefix(fullPath, "HKCU\\") } else { return nil } // Provide parent key and subkey name to DeleteKey // path: Software\...\Uninstall\AppGUID lastSlash := strings.LastIndex(subPath, `\`) if lastSlash == -1 { return nil } parentPath := subPath[:lastSlash] keyName := subPath[lastSlash+1:] k, err := registry.OpenKey(hive, parentPath, registry.WRITE) if err != nil { return err } defer k.Close() return registry.DeleteKey(k, keyName) } // RunUninstaller executes the uninstall command func RunUninstaller(cmdString string) error { fmt.Printf("RunUninstaller Called with: %s\n", cmdString) cmd := exec.Command("cmd", "/C", cmdString) cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false} // Show window so user can click next err := cmd.Start() if err != nil { fmt.Printf("RunUninstaller Error: %v\n", err) return err } fmt.Printf("RunUninstaller Started Successfully\n") return nil }