From 44d20eb98392a042e6c40b1948209dec7159b33b Mon Sep 17 00:00:00 2001
From: partisan <none@noone.no>
Date: Sat, 1 Mar 2025 17:59:26 +0100
Subject: [PATCH 1/6] clean up

---
 README.md        |  12 +-
 appindex.go      | 277 ++++++++++++++++++++++++++++++++++++++++++-----
 auto.go          |  38 +++----
 decompress.go    |   6 +-
 download.go      |  10 +-
 install.go       |   4 +-
 register_unix.go |  22 +++-
 register_win.go  |  14 ++-
 run_unix.go      |   4 -
 run_win.go       |   4 -
 10 files changed, 319 insertions(+), 72 deletions(-)

diff --git a/README.md b/README.md
index b65fd01..1a924b4 100644
--- a/README.md
+++ b/README.md
@@ -47,9 +47,9 @@ func main() {
 	}
 
 	// -- Download --
-    // spm.AutoDownloadSpecified(specs) downloads specified packages to temp dir and decompresses them, making them ready for install by running "spm.AutoInstallUpdates()".
+    // spm.DownloadSpecified(specs) downloads specified packages to temp dir and decompresses them, making them ready for install by running "spm.InstallUpdates()".
 	fmt.Println("Starting download and decompression...")
-	if err := spm.AutoDownloadSpecified(specs); err != nil {
+	if err := spm.DownloadSpecified(specs); err != nil {
 		fmt.Println("Error downloading packages:", err)
 		return
 	}
@@ -57,15 +57,15 @@ func main() {
 	fmt.Println("Download complete. Proceeding with installation...")
 
 	// -- Install --
-    // Install and Download are separate as you cannot replace running binaries on Windows. So the final move to the correct folder is done by "spm.AutoInstallUpdates()".
-	if err := spm.AutoInstallUpdates(); err != nil {
+    // Install and Download are separate as you cannot replace running binaries on Windows. So the final move to the correct folder is done by "spm.InstallUpdates()".
+	if err := spm.InstallUpdates(); err != nil {
 		fmt.Println("Error during installation:", err)
 		return
 	}
 
 	// -- Register --
-// spm.RegisterApp() is primarily used to modify the Windows registry so it recognizes Spitfire Browser as an installed program. 
-// You shouldn’t need to run it more than once during installation. Also this function requires administrative privileges on Windows to work correctly.
+	// spm.RegisterApp() is primarily used to modify the Windows registry so it recognizes Spitfire Browser as an installed program. 
+	// You shouldn’t need to run it more than once during installation. Also this function requires administrative privileges on Windows to work correctly.
 	if err := spm.RegisterApp(); err != nil {
 		fmt.Println("Error registering app:", err)
 		return
diff --git a/appindex.go b/appindex.go
index 14a77be..848d59e 100644
--- a/appindex.go
+++ b/appindex.go
@@ -6,14 +6,46 @@ import (
 	"io"
 	"net/http"
 	"os"
+	"path/filepath"
+	"sort"
 	"strings"
+
+	"gopkg.in/ini.v1"
 )
 
-const appIndexURL = "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX"
+// AppIndexEntry represents a single entry in an app index.
+type AppIndexEntry struct {
+	Name        string
+	Version     string
+	Release     string // "nightly" / "stable" / etc.
+	Arch        string // e.g. "amd64", "386"
+	OS          string // e.g. "windows", "linux"
+	Type        string // "browser", "addon", "theme", etc.
+	DownloadURL string
+}
 
-func DownloadAppIndex(dest string) error {
+// RemoteIndex represents a remote APPINDEX repository.
+type RemoteIndex struct {
+	Name string `json:"name"`
+	Link string `json:"link"`
+}
+
+var (
+	// defaultRemoteIndexes holds the default remote index.
+	defaultRemoteIndexes = []RemoteIndex{
+		{
+			Name: "default",
+			Link: "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX",
+		},
+	}
+	// remoteIndexes holds the current remote indexes in use.
+	remoteIndexes = defaultRemoteIndexes
+)
+
+// downloadAppIndex downloads an APPINDEX from the given URL and writes it to dest.
+func downloadAppIndex(url, dest string) error {
 	UpdateProgress(0, "Downloading APPINDEX")
-	resp, err := http.Get(appIndexURL)
+	resp, err := http.Get(url)
 	if err != nil {
 		return err
 	}
@@ -52,25 +84,10 @@ func DownloadAppIndex(dest string) error {
 	return nil
 }
 
-type AppIndexEntry struct {
-	Name        string
-	Version     string
-	Release     string // "nightly" / "stable" / etc.
-	Arch        string // e.g. "amd64", "386"
-	OS          string // e.g. "windows", "linux"
-	Type        string // "browser", "addon", "theme", etc.
-	DownloadURL string
-}
-
-func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
-	file, err := os.Open(filePath)
-	if err != nil {
-		return nil, err
-	}
-	defer file.Close()
-
+// parseAppIndexFromReader parses an APPINDEX from any io.Reader.
+func parseAppIndexFromReader(r io.Reader) ([]AppIndexEntry, error) {
 	var entries []AppIndexEntry
-	scanner := bufio.NewScanner(file)
+	scanner := bufio.NewScanner(r)
 	entry := AppIndexEntry{}
 
 	for scanner.Scan() {
@@ -112,12 +129,222 @@ func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
 		entries = append(entries, entry)
 	}
 
-	// Log all parsed entries
-	fmt.Printf("[INFO] Total parsed entries: %d\n", len(entries))
+	return entries, scanner.Err()
+}
+
+// parseAppIndex reads the APPINDEX file at filePath and parses its contents.
+func parseAppIndex(filePath string) ([]AppIndexEntry, error) {
+	file, err := os.Open(filePath)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	entries, err := parseAppIndexFromReader(file)
+	if err != nil {
+		return nil, err
+	}
+
+	fmt.Printf("[INFO] Total parsed entries from %s: %d\n", filePath, len(entries))
 	for _, e := range entries {
 		fmt.Printf(" - Name: %s, Release: %s, Type: %s, OS: %s, Arch: %s, Version: %s, URL: %s\n",
 			e.Name, e.Release, e.Type, e.OS, e.Arch, e.Version, e.DownloadURL)
 	}
-
-	return entries, scanner.Err()
+	return entries, nil
+}
+
+// saveIndex saves the current list of remote indexes to an INI file located in
+// the spm directory under installDir, but only if it's different from the default.
+func saveIndex() error {
+	// Only save if remoteIndexes differs from defaultRemoteIndexes.
+	if len(remoteIndexes) == len(defaultRemoteIndexes) {
+		same := true
+		for i, ri := range remoteIndexes {
+			if ri != defaultRemoteIndexes[i] {
+				same = false
+				break
+			}
+		}
+		if same {
+			return nil
+		}
+	}
+
+	installDir, err := GetInstallDir()
+	if err != nil {
+		return err
+	}
+	spmDir := filepath.Join(installDir, "spm")
+	if err := os.MkdirAll(spmDir, 0755); err != nil {
+		return err
+	}
+	filePath := filepath.Join(spmDir, "sources.ini")
+
+	cfg := ini.Empty()
+	sec, err := cfg.NewSection("RemoteIndexes")
+	if err != nil {
+		return err
+	}
+	// Save each remote index as a key/value pair.
+	for _, ri := range remoteIndexes {
+		if _, err := sec.NewKey(ri.Name, ri.Link); err != nil {
+			return err
+		}
+	}
+
+	return cfg.SaveTo(filePath)
+}
+
+// loadIndex loads the list of remote indexes from an INI file located in
+// the spm directory under installDir. If the file is missing, it sets remoteIndexes to the default.
+func loadIndex() error {
+	installDir, err := GetInstallDir()
+	if err != nil {
+		return err
+	}
+	spmDir := filepath.Join(installDir, "spm")
+	filePath := filepath.Join(spmDir, "sources.ini")
+	cfg, err := ini.Load(filePath)
+	if err != nil {
+		// If file is missing or can't be loaded, use the default.
+		remoteIndexes = defaultRemoteIndexes
+		return nil
+	}
+	sec := cfg.Section("RemoteIndexes")
+	var loaded []RemoteIndex
+	for _, key := range sec.Keys() {
+		loaded = append(loaded, RemoteIndex{
+			Name: key.Name(),
+			Link: key.Value(),
+		})
+	}
+	remoteIndexes = loaded
+	return nil
+}
+
+// UpdateIndex downloads fresh APPINDEX files from all remote sources and saves them
+// into the temp directory. If the app is registered, it loads the remote indexes
+// from the INI file (or uses the default if not available) and saves them after updating.
+func UpdateIndex() error {
+	tempDir := GetTempDir()
+	var sources []RemoteIndex
+	if IsRegistered() {
+		// Try to load persisted remote indexes.
+		if err := loadIndex(); err != nil {
+			// If loading fails, fall back to the default remote indexes.
+			sources = defaultRemoteIndexes
+		} else {
+			sources = remoteIndexes
+		}
+	} else {
+		// Not registered: use default remote indexes.
+		sources = defaultRemoteIndexes
+	}
+
+	// Download each APPINDEX file.
+	for _, ri := range sources {
+		localPath := filepath.Join(tempDir, fmt.Sprintf("appindex_%s.txt", ri.Name))
+		if err := downloadAppIndex(ri.Link, localPath); err != nil {
+			return fmt.Errorf("[WARN] AppIndex: failed downloading %s: %v", ri.Link, err)
+		}
+	}
+
+	// If registered, save the current remote indexes.
+	if IsRegistered() {
+		if err := saveIndex(); err != nil {
+			return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %v", err)
+		}
+	}
+	return nil
+}
+
+// GetIndex parses APPINDEX data from local files in the temp directory.
+// If a file is missing, it downloads the corresponding APPINDEX first.
+// If the app is registered, it loads remote indexes from the INI file.
+// Otherwise, it uses the default remote index.
+func GetIndex() ([]AppIndexEntry, error) {
+	var allEntries []AppIndexEntry
+	tempDir := GetTempDir()
+	var sources []RemoteIndex
+	if IsRegistered() {
+		if err := loadIndex(); err != nil {
+			sources = defaultRemoteIndexes
+		} else {
+			sources = remoteIndexes
+		}
+	} else {
+		sources = defaultRemoteIndexes
+	}
+
+	// For each remote source, ensure the APPINDEX file exists (downloading if needed),
+	// then parse its contents.
+	for _, ri := range sources {
+		localPath := filepath.Join(tempDir, fmt.Sprintf("appindex_%s.txt", ri.Name))
+		if _, err := os.Stat(localPath); os.IsNotExist(err) {
+			if err := downloadAppIndex(ri.Link, localPath); err != nil {
+				return nil, fmt.Errorf("[WARN] AppIndex: failed downloading %s: %v", ri.Link, err)
+			}
+		}
+		entries, err := parseAppIndex(localPath)
+		if err != nil {
+			return nil, fmt.Errorf("[WARN] AppIndex: failed parsing %s: %v", localPath, err)
+		}
+		allEntries = append(allEntries, entries...)
+	}
+	return allEntries, nil
+}
+
+// AddIndex adds a new remote index (name and link) into the list,
+// sorts the list by name, and if the app is registered, saves the updated list.
+func AddIndex(name, link string) error {
+	// If registered, load current indexes first.
+	if IsRegistered() {
+		if err := loadIndex(); err != nil {
+			return fmt.Errorf("[WARN] AppIndex: failed loading indexes: %w", err)
+		}
+	}
+
+	ri := RemoteIndex{
+		Name: name,
+		Link: link,
+	}
+	remoteIndexes = append(remoteIndexes, ri)
+	sort.Slice(remoteIndexes, func(i, j int) bool {
+		return remoteIndexes[i].Name < remoteIndexes[j].Name
+	})
+
+	// If registered, persist the changes.
+	if IsRegistered() {
+		if err := saveIndex(); err != nil {
+			return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %w", err)
+		}
+	}
+	return nil
+}
+
+// RemoveIndex removes any remote index with the given name from the list,
+// and if the app is registered, saves the updated list.
+func RemoveIndex(name string) error {
+	// If registered, load current indexes first.
+	if IsRegistered() {
+		if err := loadIndex(); err != nil {
+			return fmt.Errorf("[WARN] AppIndex: failed loading indexes: %w", err)
+		}
+	}
+
+	var updated []RemoteIndex
+	for _, ri := range remoteIndexes {
+		if ri.Name != name {
+			updated = append(updated, ri)
+		}
+	}
+	remoteIndexes = updated
+
+	// If registered, persist the changes.
+	if IsRegistered() {
+		if err := saveIndex(); err != nil {
+			return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %w", err)
+		}
+	}
+	return nil
 }
diff --git a/auto.go b/auto.go
index 92e574a..491bb66 100644
--- a/auto.go
+++ b/auto.go
@@ -2,7 +2,6 @@ package spm
 
 import (
 	"fmt"
-	"os"
 	"path/filepath"
 )
 
@@ -10,22 +9,19 @@ import (
 // but not yet moved to the final install location, cuz Windows has this stupid file locking mechanism
 var pendingUpdates []AppIndexEntry
 
-// AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against
+// DownloadUpdates downloads the APPINDEX file, parses it, compares against
 // currently installed packages, and if it finds a newer version, downloads
-// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by AutoInstallUpdates().
-func AutoDownloadUpdates() error {
+// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by InstallUpdates().
+func DownloadUpdates() error {
 	// 1) Download the APPINDEX file to a temporary location
-	appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
-	fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath)
-	err := DownloadAppIndex(appIndexPath)
+	err := UpdateIndex()
 	if err != nil {
 		return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err)
 	}
 	fmt.Println("[INFO] APPINDEX downloaded successfully")
 
 	// 2) Parse the APPINDEX file
-	fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath)
-	entries, err := ParseAppIndex(appIndexPath)
+	entries, err := GetIndex()
 	if err != nil {
 		return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
 	}
@@ -84,7 +80,7 @@ func AutoDownloadUpdates() error {
 		downloadDir := GetTempDir()
 		fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir)
 
-		err = DownloadPackageFromAppIndex(appIndexPath, matchingEntry.Name, matchingEntry.Release, matchingEntry.Type, downloadDir)
+		err = DownloadPackageFromAppIndex(matchingEntry.Name, matchingEntry.Release, matchingEntry.Type, downloadDir)
 		if err != nil {
 			return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err)
 		}
@@ -99,7 +95,7 @@ func AutoDownloadUpdates() error {
 		}
 		fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
 
-		// 7) Store in pendingUpdates so that AutoInstallUpdates can finish the job
+		// 7) Store in pendingUpdates so that InstallUpdates can finish the job
 		fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
 		pendingUpdates = append(pendingUpdates, AppIndexEntry{
 			Name:    matchingEntry.Name,
@@ -111,13 +107,13 @@ func AutoDownloadUpdates() error {
 		})
 	}
 
-	fmt.Println("[INFO] AutoDownloadUpdates completed successfully")
+	fmt.Println("[INFO] DownloadUpdates completed successfully")
 	return nil
 }
 
-// AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates.
+// InstallUpdates installs any packages that were downloaded and decompressed by DownloadUpdates.
 // It moves files from their temp directories to the final location and updates installed.ini.
-func AutoInstallUpdates() error {
+func InstallUpdates() error {
 	installDir, err := GetInstallDir()
 	if err != nil {
 		return err
@@ -146,7 +142,7 @@ func AutoInstallUpdates() error {
 		}
 
 		// 5) Finalize
-		err = FinalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS)
+		err = finalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS)
 		if err != nil {
 			return fmt.Errorf("failed to finalize install for %s: %w", entry.Name, err)
 		}
@@ -156,18 +152,15 @@ func AutoInstallUpdates() error {
 	return nil
 }
 
-func AutoDownloadSpecified(specs []AppIndexEntry) error {
+func DownloadSpecified(specs []AppIndexEntry) error {
 	// 1) Download the APPINDEX file to a temporary location
-	appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
-	fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath)
-	if err := DownloadAppIndex(appIndexPath); err != nil {
+	if err := UpdateIndex(); err != nil {
 		return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err)
 	}
 	fmt.Println("[INFO] APPINDEX downloaded successfully")
 
 	// 2) Parse the APPINDEX file
-	fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath)
-	entries, err := ParseAppIndex(appIndexPath)
+	entries, err := GetIndex()
 	if err != nil {
 		return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
 	}
@@ -225,7 +218,6 @@ func AutoDownloadSpecified(specs []AppIndexEntry) error {
 		downloadDir := GetTempDir()
 		fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir)
 		if err := DownloadPackageFromAppIndex(
-			appIndexPath,
 			matchingEntry.Name,
 			matchingEntry.Release,
 			matchingEntry.Type,
@@ -251,7 +243,7 @@ func AutoDownloadSpecified(specs []AppIndexEntry) error {
 		}
 		fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
 
-		// 7) Store in pendingUpdates for AutoInstallUpdates
+		// 7) Store in pendingUpdates for InstallUpdates
 		fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
 		pendingUpdates = append(pendingUpdates, *matchingEntry)
 	}
diff --git a/decompress.go b/decompress.go
index 7f7a207..00003e2 100644
--- a/decompress.go
+++ b/decompress.go
@@ -108,7 +108,11 @@ func decompressTarGz(srcFile, destDir string, updateProgress func(int, string))
 		// Update progress after extracting each file.
 		if updateProgress != nil {
 			percent := int((progressReader.BytesRead * 100) / totalSize)
-			updateProgress(percent, fmt.Sprintf("Extracted: %s", header.Name))
+			name := header.Name
+			if len(name) > 50 {
+				name = name[len(name)-50:]
+			}
+			updateProgress(percent, fmt.Sprintf("Extracted: %s", name))
 		}
 	}
 
diff --git a/download.go b/download.go
index 30d239c..c6464f2 100644
--- a/download.go
+++ b/download.go
@@ -11,9 +11,9 @@ import (
 )
 
 // DownloadPackageFromAppIndex selects and downloads the correct package from the APPINDEX.
-func DownloadPackageFromAppIndex(appIndexPath string, packageName string, release string, pkgType string, destDir string) error {
+func DownloadPackageFromAppIndex(packageName string, release string, pkgType string, destDir string) error {
 	// Parse the APPINDEX
-	entries, err := ParseAppIndex(appIndexPath)
+	entries, err := GetIndex()
 	if err != nil {
 		return fmt.Errorf("failed to parse APPINDEX: %w", err)
 	}
@@ -99,8 +99,8 @@ func DownloadPackageFromAppIndex(appIndexPath string, packageName string, releas
 
 	expectedFilePath := filepath.Join(destDir, expectedFileName)
 
-	// I dont know why is this happening, I dont want to know but sometimes some process is helding up the donwloaded files so thats why it retries here
-	maxRetries := 5
+	// I dont know why is this happening, I dont want to know but sometimes some process is helding up the downloaded files so thats why it retries here
+	maxRetries := 10
 	for i := 0; i < maxRetries; i++ {
 		err = os.Rename(downloadedFilePath, expectedFilePath)
 		if err == nil {
@@ -115,7 +115,7 @@ func DownloadPackageFromAppIndex(appIndexPath string, packageName string, releas
 		f.Close()
 
 		if i < maxRetries-1 {
-			time.Sleep(500 * time.Millisecond) // Wait before retrying
+			time.Sleep(250 * time.Millisecond) // Wait before retrying
 		}
 	}
 
diff --git a/install.go b/install.go
index 9fbf274..a8d8521 100644
--- a/install.go
+++ b/install.go
@@ -287,8 +287,8 @@ func copyFile(src, dst string) error {
 	return os.Chmod(dst, info.Mode())
 }
 
-// FinalizeInstall finalizes the installation by updating installed.ini.
-func FinalizeInstall(packageName, release, version, arch, osName string) error {
+// finalizeInstall finalizes the installation by updating installed.ini.
+func finalizeInstall(packageName, release, version, arch, osName string) error {
 	installDir, err := GetInstallDir()
 	if err != nil {
 		return err
diff --git a/register_unix.go b/register_unix.go
index 6568edd..d3911bf 100644
--- a/register_unix.go
+++ b/register_unix.go
@@ -4,7 +4,11 @@
 
 package spm
 
-import "fmt"
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+)
 
 // RegisterApp is not supported on non-Windows platforms.
 func RegisterApp() error {
@@ -15,3 +19,19 @@ func RegisterApp() error {
 func UnregisterApp() error {
 	return fmt.Errorf("UnregisterApp is only available on Windows")
 }
+
+// IsRegistered returns true if the application is detected as installed.
+// On Linux, we assume it is installed if the main executable exists in the install directory.
+func IsRegistered() bool {
+	installDir, err := GetInstallDir()
+	if err != nil {
+		return false
+	}
+
+	// Assume the executable is named "spitfire" and is located in installDir.
+	exePath := filepath.Join(installDir, "browser", "spitfire")
+	if _, err := os.Stat(exePath); err == nil {
+		return true
+	}
+	return false
+}
diff --git a/register_win.go b/register_win.go
index b2cf47f..206f0ae 100644
--- a/register_win.go
+++ b/register_win.go
@@ -10,7 +10,7 @@ import (
 	"golang.org/x/sys/windows/registry"
 )
 
-// RegisterApp writes the necessary registry keys, making it appear as offically installed app
+// RegisterApp writes the necessary registry keys, making it appear as officially installed app
 func RegisterApp() error {
 	exePath, err := GetInstallDir()
 	if err != nil {
@@ -133,3 +133,15 @@ func deleteRegistryTree(root registry.Key, path string) error {
 	// Finally, delete the (now empty) key.
 	return registry.DeleteKey(root, path)
 }
+
+// IsRegistered returns true if the application is registered (installed) in the registry.
+func IsRegistered() bool {
+	// Try to open the uninstall key with read-only access.
+	key, err := registry.OpenKey(registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall\SpitfireBrowser`, registry.READ)
+	if err != nil {
+		// If the key cannot be opened, assume the app is not registered.
+		return false
+	}
+	defer key.Close()
+	return true
+}
diff --git a/run_unix.go b/run_unix.go
index cc82b79..aa580b2 100644
--- a/run_unix.go
+++ b/run_unix.go
@@ -9,7 +9,6 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"runtime"
 	"syscall"
 )
 
@@ -21,9 +20,6 @@ func Run() error {
 	}
 
 	exePath := filepath.Join(installDir, "browser", "spitfire.exe")
-	if runtime.GOOS != "windows" {
-		exePath = filepath.Join(installDir, "browser", "spitfire")
-	}
 
 	cmd := exec.Command(exePath)
 	cmd.Dir = filepath.Join(installDir, "browser")
diff --git a/run_win.go b/run_win.go
index d1f2b38..a108d7e 100644
--- a/run_win.go
+++ b/run_win.go
@@ -9,7 +9,6 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"runtime"
 	"syscall"
 )
 
@@ -21,9 +20,6 @@ func Run() error {
 	}
 
 	exePath := filepath.Join(installDir, "browser", "spitfire.exe")
-	if runtime.GOOS != "windows" {
-		exePath = filepath.Join(installDir, "browser", "spitfire")
-	}
 
 	cmd := exec.Command(exePath)
 	cmd.Dir = filepath.Join(installDir, "browser")

From f3a78f4bd6e8a9df88b46b7add2a359f57f07777 Mon Sep 17 00:00:00 2001
From: partisan <none@noone.no>
Date: Sun, 2 Mar 2025 09:59:00 +0100
Subject: [PATCH 2/6] Fixed error loops

---
 README.md        |  1 -
 auto.go          | 33 ++++++++++++++++-----------------
 register_unix.go |  8 ++++----
 3 files changed, 20 insertions(+), 22 deletions(-)

diff --git a/README.md b/README.md
index 1a924b4..63f036c 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,6 @@ func main() {
     spm.Run()
 }
 ```
-*Functions, and specifically their names, are subject to change as I really don't like "AutoDownloadSpecified," but I don't want to make it "DownloadSpecified" yet, as there are still many functions in SPM used for manual downloads, installs, etc.*
 
 <p align="center" style="font-size: 32px;">
   <strong>License</strong>
diff --git a/auto.go b/auto.go
index 491bb66..d544a44 100644
--- a/auto.go
+++ b/auto.go
@@ -196,23 +196,22 @@ func DownloadSpecified(specs []AppIndexEntry) error {
 		}
 		fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry)
 
-		// // Check if an update is needed
-		// updateNeeded, err := IsUpdateNeeded(
-		// 	installDir,
-		// 	matchingEntry.Name,
-		// 	matchingEntry.Release,
-		// 	matchingEntry.Version,
-		// 	matchingEntry.Arch,
-		// 	matchingEntry.OS,
-		// )
-		// if err != nil {
-		// 	return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err)
-		// }
+		updateNeeded, err := IsUpdateNeeded(
+			installDir,
+			matchingEntry.Name,
+			matchingEntry.Release,
+			matchingEntry.Version,
+			matchingEntry.Arch,
+			matchingEntry.OS,
+		)
+		if err != nil {
+			return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err)
+		}
 
-		// if !updateNeeded {
-		// 	fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name)
-		// 	continue
-		// }
+		if !updateNeeded {
+			fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name)
+			continue
+		}
 
 		// 5) Download the package
 		downloadDir := GetTempDir()
@@ -243,7 +242,7 @@ func DownloadSpecified(specs []AppIndexEntry) error {
 		}
 		fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
 
-		// 7) Store in pendingUpdates for InstallUpdates
+		// Add to pendingUpdates for InstallUpdates
 		fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
 		pendingUpdates = append(pendingUpdates, *matchingEntry)
 	}
diff --git a/register_unix.go b/register_unix.go
index d3911bf..fb860ee 100644
--- a/register_unix.go
+++ b/register_unix.go
@@ -11,13 +11,13 @@ import (
 )
 
 // RegisterApp is not supported on non-Windows platforms.
-func RegisterApp() error {
-	return fmt.Errorf("RegisterApp is only available on Windows")
+func RegisterApp() {
+	fmt.Println("[WARN] RegisterApp() is only available on Windows")
 }
 
 // UnregisterApp is not supported on non-Windows platforms.
-func UnregisterApp() error {
-	return fmt.Errorf("UnregisterApp is only available on Windows")
+func UnregisterApp() {
+	fmt.Println("[WARN] UnregisterApp() is only available on Windows")
 }
 
 // IsRegistered returns true if the application is detected as installed.

From f34f335206915b05f6190560418df9d26dc15a06 Mon Sep 17 00:00:00 2001
From: partisan <none@noone.no>
Date: Sun, 2 Mar 2025 10:09:18 +0100
Subject: [PATCH 3/6] Reverted changes bcs of './installer.go:115:13:
 spm.RegisterApp() (no value) used as value'

---
 register_unix.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/register_unix.go b/register_unix.go
index fb860ee..f9e35c9 100644
--- a/register_unix.go
+++ b/register_unix.go
@@ -11,13 +11,13 @@ import (
 )
 
 // RegisterApp is not supported on non-Windows platforms.
-func RegisterApp() {
-	fmt.Println("[WARN] RegisterApp() is only available on Windows")
+func RegisterApp() error {
+	return fmt.Errorf("[WARN] RegisterApp() is only available on Windows")
 }
 
 // UnregisterApp is not supported on non-Windows platforms.
-func UnregisterApp() {
-	fmt.Println("[WARN] UnregisterApp() is only available on Windows")
+func UnregisterApp() error {
+	return fmt.Errorf("[WARN] UnregisterApp() is only available on Windows")
 }
 
 // IsRegistered returns true if the application is detected as installed.

From c84170d61528fa5f4d7069641d513522f51d63d2 Mon Sep 17 00:00:00 2001
From: partisan <partisan@noreply@weforge.xyz>
Date: Fri, 28 Mar 2025 09:21:38 +0000
Subject: [PATCH 4/6] Fix RunAndWait() process detection

Fixed incorrect browser exit detection in RunAndWait() which could lead to Browser file corruption due to incomplete updates.
---
 run_win.go | 28 ++++++++++++++++++----------
 1 file changed, 18 insertions(+), 10 deletions(-)

diff --git a/run_win.go b/run_win.go
index a108d7e..706b6d4 100644
--- a/run_win.go
+++ b/run_win.go
@@ -9,7 +9,10 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"syscall"
+	"strings"
+	"time"
+
+	"golang.org/x/sys/windows"
 )
 
 // Run locates and starts the installed Spitfire browser without waiting for it to exit.
@@ -33,7 +36,6 @@ func RunAndWait() error {
 		return fmt.Errorf("failed to get install directory: %w", err)
 	}
 
-	// Construct the browser executable path
 	exePath := filepath.Join(installDir, "browser", "spitfire.exe")
 	if _, err := os.Stat(exePath); err != nil {
 		return fmt.Errorf("browser executable not found at %s: %w", exePath, err)
@@ -42,23 +44,29 @@ func RunAndWait() error {
 	cmd := exec.Command(exePath)
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
+	cmd.Dir = filepath.Join(installDir, "browser")
 
-	// Use CREATE_NEW_PROCESS_GROUP flag for Windows
-	cmd.SysProcAttr = &syscall.SysProcAttr{
-		CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
+	// Create job object starting the process
+	job, err := windows.CreateJobObject(nil, nil)
+	if err != nil {
+		return fmt.Errorf("failed to create job object: %w", err)
 	}
+	defer windows.CloseHandle(job)
 
 	fmt.Printf("Starting browser: %s\n", exePath)
 	if err := cmd.Start(); err != nil {
 		return fmt.Errorf("failed to start browser: %w", err)
 	}
 
-	fmt.Printf("Browser process started with PID %d\n", cmd.Process.Pid)
-
-	if err := cmd.Wait(); err != nil {
-		return fmt.Errorf("browser exited with error: %w", err)
+	for {
+		cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq spitfire.exe")
+		output, _ := cmd.Output()
+		if !strings.Contains(string(output), "spitfire.exe") {
+			break
+		}
+		time.Sleep(1 * time.Second)
 	}
 
-	fmt.Println("Browser exited successfully.")
+	fmt.Println("Browser exited.")
 	return nil
 }

From b31ea5f3ccd49e9ec640a566adebf5d2d8ff179c Mon Sep 17 00:00:00 2001
From: partisan <partisan@noreply@weforge.xyz>
Date: Fri, 28 Mar 2025 09:33:17 +0000
Subject: [PATCH 5/6] Update go.sum

---
 go.sum | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/go.sum b/go.sum
index be248fb..bd54818 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

From af129cb59d36b3d003b486be340309c30854463d Mon Sep 17 00:00:00 2001
From: partisan <partisan@noreply@weforge.xyz>
Date: Fri, 28 Mar 2025 09:33:36 +0000
Subject: [PATCH 6/6] Update go.mod

---
 go.mod | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 8b67881..f4c1524 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,12 @@
 module weforge.xyz/Spitfire/SPM
 
-go 1.21
+go 1.23.0
 
-require gopkg.in/ini.v1 v1.67.0
+toolchain go1.24.1
+
+require (
+	golang.org/x/sys v0.31.0
+	gopkg.in/ini.v1 v1.67.0
+)
 
 require github.com/stretchr/testify v1.10.0 // indirect