diff options
| author | spiperac <[email protected]> | 2026-06-15 06:42:13 +0200 |
|---|---|---|
| committer | spiperac <[email protected]> | 2026-06-15 06:42:13 +0200 |
| commit | 0a477fd288d064d265395c88dbd0f261b723804e (patch) | |
| tree | 9b129f8f41c907285b89ed8385679934bb59c14f /internal/ui/browser.go | |
Initial release
Diffstat (limited to 'internal/ui/browser.go')
| -rw-r--r-- | internal/ui/browser.go | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/internal/ui/browser.go b/internal/ui/browser.go new file mode 100644 index 0000000..2dddb0f --- /dev/null +++ b/internal/ui/browser.go @@ -0,0 +1,283 @@ +package ui + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/dialog" + + "github.com/shiv/internal/logger" +) + +const prefKeyDefaultBrowser = "default_browser" + +// DetectedBrowser holds a browser's display name and executable path. +type DetectedBrowser struct { + Name string + Path string +} + +// isFirefox returns true if the executable looks like Firefox. +func isFirefox(path string) bool { + base := strings.ToLower(filepath.Base(path)) + return strings.Contains(base, "firefox") +} + +// DetectBrowsers scans the system for supported browsers and returns all found. +func DetectBrowsers() []DetectedBrowser { + switch runtime.GOOS { + case "linux": + return detectBrowsersLinux() + case "darwin": + return detectBrowsersDarwin() + case "windows": + return detectBrowsersWindows() + } + return nil +} + +func detectBrowsersLinux() []DetectedBrowser { + candidates := []struct{ name, bin string }{ + {"Google Chrome", "google-chrome"}, + {"Google Chrome (stable)", "google-chrome-stable"}, + {"Chromium", "chromium"}, + {"Chromium (browser)", "chromium-browser"}, + {"Brave (Browser)", "brave-browser"}, + {"Brave", "brave"}, + {"Microsoft Edge", "microsoft-edge"}, + {"Vivaldi", "vivaldi"}, + {"Vivaldi (stable)", "vivaldi-Stable"}, + {"Opera", "opera"}, + {"Opera (stable)", "opera-stable"}, + {"Opera (browser)", "opera-browser"}, + {"Firefox", "firefox"}, + {"Firefox ESR", "firefox-esr"}, + } + var found []DetectedBrowser + seen := map[string]bool{} + for _, c := range candidates { + path, err := exec.LookPath(c.bin) + if err != nil { + continue + } + if seen[path] { + continue + } + seen[path] = true + found = append(found, DetectedBrowser{Name: c.name, Path: path}) + } + return found +} + +func detectBrowsersDarwin() []DetectedBrowser { + candidates := []struct{ name, app string }{ + {"Google Chrome", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"}, + {"Chromium", "/Applications/Chromium.app/Contents/MacOS/Chromium"}, + {"Brave", "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"}, + {"Microsoft Edge", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"}, + {"Vivaldi", "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi"}, + {"Opera", "/Applications/Opera.app/Contents/MacOS/Opera"}, + {"Firefox", "/Applications/Firefox.app/Contents/MacOS/firefox"}, + } + var found []DetectedBrowser + for _, c := range candidates { + if _, err := os.Stat(c.app); err == nil { + found = append(found, DetectedBrowser{Name: c.name, Path: c.app}) + } + } + return found +} + +func detectBrowsersWindows() []DetectedBrowser { + candidates := []struct{ name, path string }{ + {"Google Chrome", `C:\Program Files\Google\Chrome\Application\chrome.exe`}, + {"Google Chrome (x86)", `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`}, + {"Chromium", `C:\Program Files\Chromium\Application\chrome.exe`}, + {"Brave", `C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe`}, + {"Microsoft Edge", `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`}, + {"Vivaldi", `C:\Program Files\Vivaldi\Application\vivaldi.exe`}, + {"Opera", `C:\Program Files\Opera\opera.exe`}, + {"Firefox", `C:\Program Files\Mozilla Firefox\firefox.exe`}, + {"Firefox (x86)", `C:\Program Files (x86)\Mozilla Firefox\firefox.exe`}, + } + var found []DetectedBrowser + seen := map[string]bool{} + for _, c := range candidates { + if _, err := os.Stat(c.path); err == nil { + if !seen[c.path] { + seen[c.path] = true + found = append(found, DetectedBrowser{Name: c.name, Path: c.path}) + } + } + } + return found +} + +// profileDir returns the persistent browser profile directory for the given browser executable. +func profileDir(fyneApp fyne.App, browserPath string) string { + root := fyneApp.Storage().RootURI().Path() + if isFirefox(browserPath) { + return filepath.Join(root, "browser-profile-firefox") + } + return filepath.Join(root, "browser-profile-chrome") +} + +// setupFirefoxProfile creates a Firefox profile dir with proxy settings written into user.js. +func setupFirefoxProfile(profilePath, proxyHost string, proxyPort int) error { + if err := os.MkdirAll(profilePath, 0700); err != nil { + return fmt.Errorf("create firefox profile dir: %w", err) + } + userJS := fmt.Sprintf(`// Shiv proxy profile — auto-generated, do not edit manually. +user_pref("network.proxy.type", 1); +user_pref("network.proxy.http", "%s"); +user_pref("network.proxy.http_port", %d); +user_pref("network.proxy.ssl", "%s"); +user_pref("network.proxy.ssl_port", %d); +user_pref("network.proxy.no_proxies_on", ""); +user_pref("browser.startup.homepage_override.mstone", "ignore"); +user_pref("browser.shell.checkDefaultBrowser", false); +user_pref("extensions.enabledScopes", 0); +user_pref("extensions.autoDisableScopes", 15); +user_pref("network.stricttransportsecurity.preloadlist", false); +user_pref("security.enterprise_roots.enabled", true); +`, proxyHost, proxyPort, proxyHost, proxyPort) + return os.WriteFile(filepath.Join(profilePath, "user.js"), []byte(userJS), 0600) +} + +// LaunchBrowser launches the given browser pointed at the proxy with an isolated profile. +func LaunchBrowser(fyneApp fyne.App, browser DetectedBrowser, proxyHost string, proxyPort int) error { + profile := profileDir(fyneApp, browser.Path) + proxyAddr := fmt.Sprintf("%s:%d", proxyHost, proxyPort) + + var args []string + + if isFirefox(browser.Path) { + if err := setupFirefoxProfile(profile, proxyHost, proxyPort); err != nil { + return err + } + args = []string{ + "-profile", profile, + "-no-remote", + } + } else { + // Chromium-based — profile dir is created automatically by the browser. + args = []string{ + "--proxy-server=http://" + proxyAddr, + "--user-data-dir=" + profile, + "--no-first-run", + "--no-default-browser-check", + "--disable-extensions", + "--disable-sync", + "--disable-background-networking", + "--disable-default-apps", + "--disable-component-update", + "--disable-breakpad", + "--metrics-recording-only", + "--disable-features=OptimizationHints,MediaRouter", + } + } + + cmd := exec.Command(browser.Path, args...) + cmd.Stderr = nil + cmd.Stdout = nil + if err := cmd.Start(); err != nil { + return fmt.Errorf("launch browser: %w", err) + } + + logger.Info("browser: launched %s (pid %d) via %s", browser.Name, cmd.Process.Pid, proxyAddr) + + // Detach — we don't want to wait for the browser to exit. + go func() { + err := cmd.Wait() + if err != nil { + logger.Error("browser exited early: %v", err) + } + }() + return nil +} + +// ClearBrowserProfile deletes the stored profile for the given browser executable. +func ClearBrowserProfile(fyneApp fyne.App, browserPath string) error { + profile := profileDir(fyneApp, browserPath) + if err := os.RemoveAll(profile); err != nil { + return fmt.Errorf("clear browser profile: %w", err) + } + return nil +} + +// launchDefaultBrowser reads the saved default browser from prefs and launches it. +// If no default is set it shows a picker dialog first. +func launchDefaultBrowser(fyneApp fyne.App, win fyne.Window) { + prefs := fyneApp.Preferences() + proxyHost := prefs.StringWithFallback(prefKeyProxyHost, defaultProxyHost) + proxyPort := prefs.IntWithFallback(prefKeyProxyPort, defaultProxyPort) + + savedPath := prefs.String(prefKeyDefaultBrowser) + if savedPath != "" { + // Verify it still exists. + if _, err := os.Stat(savedPath); err == nil { + browser := DetectedBrowser{Name: filepath.Base(savedPath), Path: savedPath} + if err := LaunchBrowser(fyneApp, browser, proxyHost, proxyPort); err != nil { + dialog.ShowError(err, win) + } + return + } + // Saved browser no longer exists — fall through to picker. + prefs.SetString(prefKeyDefaultBrowser, "") + } + + browsers := DetectBrowsers() + if len(browsers) == 0 { + dialog.ShowInformation("No Browser Found", + "No supported browser was detected on your system.\nInstall Chrome, Brave, Firefox, or another Chromium-based browser.", + win) + return + } + + // If only one browser found, use it directly and save as default. + if len(browsers) == 1 { + prefs.SetString(prefKeyDefaultBrowser, browsers[0].Path) + if err := LaunchBrowser(fyneApp, browsers[0], proxyHost, proxyPort); err != nil { + dialog.ShowError(err, win) + } + return + } + + // Multiple browsers — show picker. + showBrowserPickerDialog(fyneApp, win, browsers, proxyHost, proxyPort) +} + +// showBrowserPickerDialog shows a one-time dialog to pick a default browser. +func showBrowserPickerDialog(fyneApp fyne.App, win fyne.Window, browsers []DetectedBrowser, proxyHost string, proxyPort int) { + names := make([]string, len(browsers)) + for i, b := range browsers { + names[i] = b.Name + } + + selected := browsers[0] + picker := newBrowserPickerContent(names, func(name string) { + for _, b := range browsers { + if b.Name == name { + selected = b + break + } + } + }) + + d := dialog.NewCustomConfirm("Select Default Browser", "Launch", "Cancel", picker, func(confirmed bool) { + if !confirmed { + return + } + fyneApp.Preferences().SetString(prefKeyDefaultBrowser, selected.Path) + if err := LaunchBrowser(fyneApp, selected, proxyHost, proxyPort); err != nil { + dialog.ShowError(err, win) + } + }, win) + closeOnEscape(win, d.Dismiss) + d.Show() +} |
