diff --git a/installer.go b/installer.go index 57c4f2f..0146ac7 100644 --- a/installer.go +++ b/installer.go @@ -109,6 +109,13 @@ func (inst *Installer) doFinalInstall() { return } + // Register the app in Windows (i.e. create registry entries) + spm.UpdateProgress(0, "Registering app...") + if err := spm.RegisterApp(); err != nil { + inst.LastError = err + return + } + spm.UpdateProgress(100, "Installation complete!") }() } diff --git a/main.go b/main.go index a267f20..bcb1225 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,11 @@ package main import ( + "flag" "fmt" - "math" "os" + "time" + "spitfire-installer/spm" rl "github.com/gen2brain/raylib-go/raylib" @@ -30,7 +32,14 @@ var ( const finalStep = 3 func main() { - spm.Run() + // Check for the --silent flag. + silent := flag.Bool("silent", false, "run installer without GUI") + flag.Parse() + + if *silent { + runSilent() + return + } monitor := rl.GetCurrentMonitor() if monitor < 0 { @@ -96,8 +105,8 @@ func main() { radius := float32(30) dx := mousePos.X - topRightX dy := mousePos.Y - topRightY - dist := float32(math.Sqrt(float64(dx*dx + dy*dy))) - if dist < radius+30 { + dist := float32((dx*dx + dy*dy)) + if dist < (radius+30)*(radius+30) { textHoverFade += 0.1 if textHoverFade > 1 { textHoverFade = 1 @@ -161,6 +170,36 @@ func main() { } } +func runSilent() { + fmt.Println("Running installer in silent mode (default values will be used).") + + // Start the download+decompress phase. + installer.StartDownloadDecompress() + + // Poll until the download/decompression phase is done. + for !installer.DoneDownload { + time.Sleep(1 * time.Second) + } + + // Start the final installation. + installer.FinalInstall() + + // Poll until installation is finished. + for !installer.DoneInstall { + time.Sleep(1 * time.Second) + } + + if installer.LastError != nil { + fmt.Printf("Installation failed: %v\n", installer.LastError) + os.Exit(1) + } + + fmt.Println("Installation complete!") + // Optionally launch the app after install: + // spm.Run() or another function if needed. + os.Exit(0) +} + func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, prevX, prevY, nextX, nextY int32) { switch currentStep { case 0: @@ -211,7 +250,7 @@ func drawHeader(screenW int) { } // Now we pass "displayProgress" (0..100) as a float, "posAlpha" in [0..1], -// plus "doneInstall" to show the "Run App" button +// plus "doneInstall" to show the "Run App" button. func drawInstallCircle(screenW, screenH int, posAlpha, displayProgress float32, doneInstall bool, hoverAlpha float32) { // Lerp position/radius topRight := rl.Vector2{X: float32(screenW - 80), Y: 100} @@ -251,7 +290,7 @@ func drawInstallCircle(screenW, screenH int, posAlpha, displayProgress float32, } } -// "Run App" button logic is the same +// "Run App" button logic is the same. func drawRunAppButton(cx, cy float32) { w := float32(180) h := float32(50) @@ -259,7 +298,8 @@ func drawRunAppButton(cx, cy float32) { hovered := overRect(rl.GetMousePosition(), rect) drawRoundedRectButton(rect, "Start Spitfire", 1.0, hovered) if hovered && rl.IsMouseButtonPressed(rl.MouseLeftButton) { - fmt.Println("Launching the app (placeholder)...") + fmt.Println("Launching...") + spm.Run() os.Exit(0) } } @@ -346,85 +386,3 @@ func startTransition(from, to int) { targetStep = to transition.Start(from, to) } - -// SPM example - -// package main - -// import ( -// "fmt" -// "os" -// "path/filepath" -// "spitfire-installer/spm" -// "time" -// ) - -// func main() { -// // Start a goroutine to display progress updates -// done := make(chan bool) -// go func() { -// for { -// select { -// case <-done: -// return -// default: -// percentage, task := spm.GetProgress() -// fmt.Printf("\r[%3d%%] %s", percentage, task) -// time.Sleep(500 * time.Millisecond) -// } -// } -// }() - -// // Set up the download directory -// downloadDir := spm.GetTempDownloadDir() -// fmt.Println("\nTemporary download directory:", downloadDir) - -// // Download the APPINDEX -// appIndexPath := filepath.Join(downloadDir, "APPINDEX") -// spm.UpdateProgress(0, "Starting APPINDEX download") -// if err := spm.DownloadAppIndex(appIndexPath); err != nil { -// fmt.Println("\nError downloading APPINDEX:", err) -// done <- true -// os.Exit(1) -// } - -// // Download the desired package version (e.g., nightly) -// packageName := "spitfire-browser" -// release := "nightly" - -// spm.UpdateProgress(0, "Starting package download") -// if err := spm.DownloadPackageFromAppIndex(appIndexPath, packageName, release, downloadDir); err != nil { -// fmt.Println("\nError downloading package:", err) -// done <- true -// os.Exit(1) -// } - -// // Decompress and install -// packagePath := filepath.Join(downloadDir, "browser-amd64-nightly-linux.tar.gz") -// spm.UpdateProgress(0, "Starting decompression") -// tempDir, err := spm.DecompressToTemp(packagePath) -// if err != nil { -// fmt.Println("\nError decompressing package:", err) -// done <- true -// os.Exit(1) -// } -// fmt.Println("\nDecompressed package to:", tempDir) - -// // Generate default install directory -// installDir, err := spm.GetDefaultInstallDir() -// if err != nil { -// inst.LastError = fmt.Errorf("failed to determine default install directory: %w", err) -// return -// } - -// spm.UpdateProgress(0, "Starting installation") -// if err := spm.MoveFilesToInstallDir(tempDir, installDir); err != nil { -// fmt.Println("\nError installing package:", err) -// done <- true -// os.Exit(1) -// } - -// // Notify progress display to stop and finalize -// done <- true -// fmt.Printf("\nSuccessfully installed %s (%s) to %s\n", packageName, release, installDir) -// } diff --git a/spm/decompress.go b/spm/decompress.go index dcfb9f8..7f7a207 100644 --- a/spm/decompress.go +++ b/spm/decompress.go @@ -10,7 +10,7 @@ import ( "strings" ) -// DecompressPackage determines the package format and decompresses it +// DecompressPackage now passes UpdateProgress to decompressTarGz. func DecompressPackage(downloadDir, packageName, arch, osName, pkgType, release, version string) (string, error) { // 1) Construct the .tar.gz name expectedFileName := fmt.Sprintf( @@ -35,7 +35,7 @@ func DecompressPackage(downloadDir, packageName, arch, osName, pkgType, release, } // 3) Decompress everything into `decompressDir` - if err := decompressTarGz(packagePath, decompressDir); err != nil { + if err := decompressTarGz(packagePath, decompressDir, UpdateProgress); err != nil { return "", fmt.Errorf("failed to decompress: %w", err) } @@ -43,14 +43,28 @@ func DecompressPackage(downloadDir, packageName, arch, osName, pkgType, release, return decompressDir, nil } -func decompressTarGz(srcFile, destDir string) error { +// decompressTarGz takes an additional updateProgress callback to report progress. +func decompressTarGz(srcFile, destDir string, updateProgress func(int, string)) error { f, err := os.Open(srcFile) if err != nil { return err } defer f.Close() - gzr, err := gzip.NewReader(f) + fileInfo, err := f.Stat() + if err != nil { + return err + } + totalSize := fileInfo.Size() + + // Wrap the file reader so we can track progress. + progressReader := &ProgressReader{ + Reader: f, + Total: totalSize, + Callback: updateProgress, + } + + gzr, err := gzip.NewReader(progressReader) if err != nil { return err } @@ -73,6 +87,11 @@ func decompressTarGz(srcFile, destDir string) error { return err } case tar.TypeReg: + outPath := filepath.Join(destDir, header.Name) + // Ensure the parent directory exists + if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { + return err + } outFile, err := os.Create(outPath) if err != nil { return err @@ -83,8 +102,38 @@ func decompressTarGz(srcFile, destDir string) error { return err } default: - // huh + // ignoring other types for now + } + + // Update progress after extracting each file. + if updateProgress != nil { + percent := int((progressReader.BytesRead * 100) / totalSize) + updateProgress(percent, fmt.Sprintf("Extracted: %s", header.Name)) } } + + // Final update: extraction complete. + if updateProgress != nil { + updateProgress(100, "Extraction complete") + } + return nil } + +// ProgressReader wraps an io.Reader to count bytes and update progress. +type ProgressReader struct { + io.Reader + Total int64 + BytesRead int64 + Callback func(int, string) +} + +func (pr *ProgressReader) Read(p []byte) (int, error) { + n, err := pr.Reader.Read(p) + pr.BytesRead += int64(n) + if pr.Callback != nil && pr.Total > 0 { + percent := int((pr.BytesRead * 100) / pr.Total) + pr.Callback(percent, "Decompressing...") + } + return n, err +} diff --git a/spm/register_unix.go b/spm/register_unix.go new file mode 100644 index 0000000..6568edd --- /dev/null +++ b/spm/register_unix.go @@ -0,0 +1,17 @@ +// run_default.go +//go:build !windows +// +build !windows + +package spm + +import "fmt" + +// RegisterApp is not supported on non-Windows platforms. +func RegisterApp() error { + return fmt.Errorf("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") +} diff --git a/spm/register_win.go b/spm/register_win.go new file mode 100644 index 0000000..b2cf47f --- /dev/null +++ b/spm/register_win.go @@ -0,0 +1,135 @@ +// run_windows.go +//go:build windows +// +build windows + +package spm + +import ( + "fmt" + + "golang.org/x/sys/windows/registry" +) + +// RegisterApp writes the necessary registry keys, making it appear as offically installed app +func RegisterApp() error { + exePath, err := GetInstallDir() + if err != nil { + return fmt.Errorf("failed to get install directory: %w", err) + } + + // 1. Create Uninstall/Modify entry + uninstallKeyPath := `Software\Microsoft\Windows\CurrentVersion\Uninstall\SpitfireBrowser` + uk, _, err := registry.CreateKey(registry.LOCAL_MACHINE, uninstallKeyPath, registry.ALL_ACCESS) + if err != nil { + return fmt.Errorf("failed to create uninstall key: %w", err) + } + defer uk.Close() + + if err := uk.SetStringValue("DisplayName", "Spitfire"); err != nil { + return err + } + if err := uk.SetStringValue("UninstallString", exePath+" --uninstall"); err != nil { + return err + } + if err := uk.SetStringValue("ModifyPath", exePath+" --modify"); err != nil { + return err + } + if err := uk.SetStringValue("DisplayIcon", exePath); err != nil { + return err + } + + // 2. Register as a browser for default apps + clientKeyPath := `Software\Clients\StartMenuInternet\SpitfireBrowser` + ck, _, err := registry.CreateKey(registry.LOCAL_MACHINE, clientKeyPath, registry.ALL_ACCESS) + if err != nil { + return fmt.Errorf("failed to create client key: %w", err) + } + defer ck.Close() + + if err := ck.SetStringValue("", "Spitfire"); err != nil { + return err + } + + // Create Capabilities subkey + capabilitiesKeyPath := clientKeyPath + `\Capabilities` + capk, _, err := registry.CreateKey(registry.LOCAL_MACHINE, capabilitiesKeyPath, registry.ALL_ACCESS) + if err != nil { + return fmt.Errorf("failed to create capabilities key: %w", err) + } + defer capk.Close() + + if err := capk.SetStringValue("ApplicationName", "Spitfire"); err != nil { + return err + } + if err := capk.SetStringValue("ApplicationDescription", "A custom browser"); err != nil { + return err + } + + // Set file associations + assocKeyPath := capabilitiesKeyPath + `\FileAssociations` + ak, _, err := registry.CreateKey(registry.LOCAL_MACHINE, assocKeyPath, registry.ALL_ACCESS) + if err != nil { + return fmt.Errorf("failed to create file associations key: %w", err) + } + defer ak.Close() + + associations := map[string]string{ + ".html": "SpitfireBrowserHTML", + "HTTP": "SpitfireBrowserHTML", + "HTTPS": "SpitfireBrowserHTML", + } + for ext, progID := range associations { + if err := ak.SetStringValue(ext, progID); err != nil { + return err + } + } + + return nil +} + +// UnregisterApp removes the registry entries created by registerApp. +func UnregisterApp() error { + // Remove the Uninstall/Modify entry. + uninstallKeyPath := `Software\Microsoft\Windows\CurrentVersion\Uninstall\SpitfireBrowser` + if err := deleteRegistryTree(registry.LOCAL_MACHINE, uninstallKeyPath); err != nil { + return fmt.Errorf("failed to delete uninstall key: %w", err) + } + + // Remove the browser registration entry. + clientKeyPath := `Software\Clients\StartMenuInternet\SpitfireBrowser` + if err := deleteRegistryTree(registry.LOCAL_MACHINE, clientKeyPath); err != nil { + return fmt.Errorf("failed to delete client key: %w", err) + } + + return nil +} + +// deleteRegistryTree recursively deletes a registry key and all its subkeys. +func deleteRegistryTree(root registry.Key, path string) error { + // Open the key with ALL_ACCESS permissions. + key, err := registry.OpenKey(root, path, registry.ALL_ACCESS) + if err != nil { + // If the key does not exist, there's nothing to do. + if err == registry.ErrNotExist { + return nil + } + return err + } + // Read the names of all subkeys. + subKeys, err := key.ReadSubKeyNames(-1) + key.Close() // Close the key so it can be deleted later. + if err != nil { + return err + } + + // Recursively delete each subkey. + for _, subKey := range subKeys { + subKeyPath := path + `\` + subKey + if err := deleteRegistryTree(root, subKeyPath); err != nil { + return err + } + } + + // Finally, delete the (now empty) key. + return registry.DeleteKey(root, path) +} diff --git a/spm/run_unix.go b/spm/run_unix.go new file mode 100644 index 0000000..cc82b79 --- /dev/null +++ b/spm/run_unix.go @@ -0,0 +1,74 @@ +// run_unix.go +//go:build !windows +// +build !windows + +package spm + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "syscall" +) + +// Run locates and starts the installed Spitfire browser without waiting for it to exit. +func Run() error { + installDir, err := GetInstallDir() + if err != nil { + return err + } + + 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") + return cmd.Start() +} + +// RunAndWait locates and starts the installed Spitfire browser and waits for it to exit. +func RunAndWait() error { + installDir, err := GetInstallDir() + if err != nil { + return fmt.Errorf("failed to get install directory: %w", err) + } + + // Construct the browser executable path + exePath := filepath.Join(installDir, "browser", "spitfire") + if _, err := os.Stat(exePath); err != nil { + return fmt.Errorf("browser executable not found at %s: %w", exePath, err) + } + + cmd := exec.Command(exePath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Start the process in a new process group + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + + fmt.Printf("Starting browser: %s\n", exePath) + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start browser: %w", err) + } + + // Print PID and PGID for debugging + pgid, err := syscall.Getpgid(cmd.Process.Pid) + if err == nil { + fmt.Printf("Browser process started with PID %d (PGID %d)\n", cmd.Process.Pid, pgid) + } else { + 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) + } + + fmt.Println("Browser exited successfully.") + return nil +} diff --git a/spm/run.go b/spm/run_win.go similarity index 75% rename from spm/run.go rename to spm/run_win.go index 5c6ce34..d1f2b38 100644 --- a/spm/run.go +++ b/spm/run_win.go @@ -1,71 +1,68 @@ -package spm - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" -) - -// Run locates and starts the installed Spitfire browser without waiting for it to exit. -func Run() error { - installDir, err := GetInstallDir() - if err != nil { - return err - } - - 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") - return cmd.Start() -} - -// RunAndWait locates and starts the installed Spitfire browser and waits for it to exit. -func RunAndWait() error { - installDir, err := GetInstallDir() - if err != nil { - return fmt.Errorf("failed to get install directory: %w", err) - } - - // Construct the browser executable path - exePath := filepath.Join(installDir, "browser", "spitfire.exe") - if runtime.GOOS != "windows" { - exePath = filepath.Join(installDir, "browser", "spitfire") - } - - // Check if the browser executable exists - if _, err := os.Stat(exePath); err != nil { - return fmt.Errorf("browser executable not found at %s: %w", exePath, err) - } - - // Create the command - cmd := exec.Command(exePath) - - // Attach standard output and error for debugging - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - // Start the browser process - fmt.Printf("Starting browser: %s\n", exePath) - err = cmd.Start() - if err != nil { - return fmt.Errorf("failed to start browser: %w", err) - } - - // Print the PID for debugging - fmt.Printf("Browser process started with PID %d\n", cmd.Process.Pid) - - // Wait for the process to exit - err = cmd.Wait() - if err != nil { - return fmt.Errorf("browser exited with error: %w", err) - } - - fmt.Println("Browser exited successfully.") - return nil -} +// run_windows.go +//go:build windows +// +build windows + +package spm + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "syscall" +) + +// Run locates and starts the installed Spitfire browser without waiting for it to exit. +func Run() error { + installDir, err := GetInstallDir() + if err != nil { + return err + } + + 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") + return cmd.Start() +} + +// RunAndWait locates and starts the installed Spitfire browser and waits for it to exit. +func RunAndWait() error { + installDir, err := GetInstallDir() + if err != nil { + 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) + } + + cmd := exec.Command(exePath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Use CREATE_NEW_PROCESS_GROUP flag for Windows + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } + + 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) + } + + fmt.Println("Browser exited successfully.") + return nil +}