Spaces:
Paused
Paused
| // Package browser provides cross-platform functionality for opening URLs in the default web browser. | |
| // It abstracts the underlying operating system commands and provides a simple interface. | |
| package browser | |
| import ( | |
| "fmt" | |
| "os/exec" | |
| "runtime" | |
| "strings" | |
| "sync" | |
| pkgbrowser "github.com/pkg/browser" | |
| log "github.com/sirupsen/logrus" | |
| ) | |
| // incognitoMode controls whether to open URLs in incognito/private mode. | |
| // This is useful for OAuth flows where you want to use a different account. | |
| var incognitoMode bool | |
| // lastBrowserProcess stores the last opened browser process for cleanup | |
| var lastBrowserProcess *exec.Cmd | |
| var browserMutex sync.Mutex | |
| // SetIncognitoMode enables or disables incognito/private browsing mode. | |
| func SetIncognitoMode(enabled bool) { | |
| incognitoMode = enabled | |
| } | |
| // IsIncognitoMode returns whether incognito mode is enabled. | |
| func IsIncognitoMode() bool { | |
| return incognitoMode | |
| } | |
| // CloseBrowser closes the last opened browser process. | |
| func CloseBrowser() error { | |
| browserMutex.Lock() | |
| defer browserMutex.Unlock() | |
| if lastBrowserProcess == nil || lastBrowserProcess.Process == nil { | |
| return nil | |
| } | |
| err := lastBrowserProcess.Process.Kill() | |
| lastBrowserProcess = nil | |
| return err | |
| } | |
| // OpenURL opens the specified URL in the default web browser. | |
| // It uses the pkg/browser library which provides robust cross-platform support | |
| // for Windows, macOS, and Linux. | |
| // If incognito mode is enabled, it will open in a private/incognito window. | |
| // | |
| // Parameters: | |
| // - url: The URL to open. | |
| // | |
| // Returns: | |
| // - An error if the URL cannot be opened, otherwise nil. | |
| func OpenURL(url string) error { | |
| log.Debugf("Opening URL in browser: %s (incognito=%v)", url, incognitoMode) | |
| // If incognito mode is enabled, use platform-specific incognito commands | |
| if incognitoMode { | |
| log.Debug("Using incognito mode") | |
| return openURLIncognito(url) | |
| } | |
| // Use pkg/browser for cross-platform support | |
| err := pkgbrowser.OpenURL(url) | |
| if err == nil { | |
| log.Debug("Successfully opened URL using pkg/browser library") | |
| return nil | |
| } | |
| log.Debugf("pkg/browser failed: %v, trying platform-specific commands", err) | |
| // Fallback to platform-specific commands | |
| return openURLPlatformSpecific(url) | |
| } | |
| // openURLPlatformSpecific is a helper function that opens a URL using OS-specific commands. | |
| // This serves as a fallback mechanism for OpenURL. | |
| // | |
| // Parameters: | |
| // - url: The URL to open. | |
| // | |
| // Returns: | |
| // - An error if the URL cannot be opened, otherwise nil. | |
| func openURLPlatformSpecific(url string) error { | |
| var cmd *exec.Cmd | |
| switch runtime.GOOS { | |
| case "darwin": // macOS | |
| cmd = exec.Command("open", url) | |
| case "windows": | |
| cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) | |
| case "linux": | |
| // Try common Linux browsers in order of preference | |
| browsers := []string{"xdg-open", "x-www-browser", "www-browser", "firefox", "chromium", "google-chrome"} | |
| for _, browser := range browsers { | |
| if _, err := exec.LookPath(browser); err == nil { | |
| cmd = exec.Command(browser, url) | |
| break | |
| } | |
| } | |
| if cmd == nil { | |
| return fmt.Errorf("no suitable browser found on Linux system") | |
| } | |
| default: | |
| return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) | |
| } | |
| log.Debugf("Running command: %s %v", cmd.Path, cmd.Args[1:]) | |
| err := cmd.Start() | |
| if err != nil { | |
| return fmt.Errorf("failed to start browser command: %w", err) | |
| } | |
| log.Debug("Successfully opened URL using platform-specific command") | |
| return nil | |
| } | |
| // openURLIncognito opens a URL in incognito/private browsing mode. | |
| // It first tries to detect the default browser and use its incognito flag. | |
| // Falls back to a chain of known browsers if detection fails. | |
| // | |
| // Parameters: | |
| // - url: The URL to open. | |
| // | |
| // Returns: | |
| // - An error if the URL cannot be opened, otherwise nil. | |
| func openURLIncognito(url string) error { | |
| // First, try to detect and use the default browser | |
| if cmd := tryDefaultBrowserIncognito(url); cmd != nil { | |
| log.Debugf("Using detected default browser: %s %v", cmd.Path, cmd.Args[1:]) | |
| if err := cmd.Start(); err == nil { | |
| storeBrowserProcess(cmd) | |
| log.Debug("Successfully opened URL in default browser's incognito mode") | |
| return nil | |
| } | |
| log.Debugf("Failed to start default browser, trying fallback chain") | |
| } | |
| // Fallback to known browser chain | |
| cmd := tryFallbackBrowsersIncognito(url) | |
| if cmd == nil { | |
| log.Warn("No browser with incognito support found, falling back to normal mode") | |
| return openURLPlatformSpecific(url) | |
| } | |
| log.Debugf("Running incognito command: %s %v", cmd.Path, cmd.Args[1:]) | |
| err := cmd.Start() | |
| if err != nil { | |
| log.Warnf("Failed to open incognito browser: %v, falling back to normal mode", err) | |
| return openURLPlatformSpecific(url) | |
| } | |
| storeBrowserProcess(cmd) | |
| log.Debug("Successfully opened URL in incognito/private mode") | |
| return nil | |
| } | |
| // storeBrowserProcess safely stores the browser process for later cleanup. | |
| func storeBrowserProcess(cmd *exec.Cmd) { | |
| browserMutex.Lock() | |
| lastBrowserProcess = cmd | |
| browserMutex.Unlock() | |
| } | |
| // tryDefaultBrowserIncognito attempts to detect the default browser and return | |
| // an exec.Cmd configured with the appropriate incognito flag. | |
| func tryDefaultBrowserIncognito(url string) *exec.Cmd { | |
| switch runtime.GOOS { | |
| case "darwin": | |
| return tryDefaultBrowserMacOS(url) | |
| case "windows": | |
| return tryDefaultBrowserWindows(url) | |
| case "linux": | |
| return tryDefaultBrowserLinux(url) | |
| } | |
| return nil | |
| } | |
| // tryDefaultBrowserMacOS detects the default browser on macOS. | |
| func tryDefaultBrowserMacOS(url string) *exec.Cmd { | |
| // Try to get default browser from Launch Services | |
| out, err := exec.Command("defaults", "read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers").Output() | |
| if err != nil { | |
| return nil | |
| } | |
| output := string(out) | |
| var browserName string | |
| // Parse the output to find the http/https handler | |
| if containsBrowserID(output, "com.google.chrome") { | |
| browserName = "chrome" | |
| } else if containsBrowserID(output, "org.mozilla.firefox") { | |
| browserName = "firefox" | |
| } else if containsBrowserID(output, "com.apple.safari") { | |
| browserName = "safari" | |
| } else if containsBrowserID(output, "com.brave.browser") { | |
| browserName = "brave" | |
| } else if containsBrowserID(output, "com.microsoft.edgemac") { | |
| browserName = "edge" | |
| } | |
| return createMacOSIncognitoCmd(browserName, url) | |
| } | |
| // containsBrowserID checks if the LaunchServices output contains a browser ID. | |
| func containsBrowserID(output, bundleID string) bool { | |
| return strings.Contains(output, bundleID) | |
| } | |
| // createMacOSIncognitoCmd creates the appropriate incognito command for macOS browsers. | |
| func createMacOSIncognitoCmd(browserName, url string) *exec.Cmd { | |
| switch browserName { | |
| case "chrome": | |
| // Try direct path first | |
| chromePath := "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" | |
| if _, err := exec.LookPath(chromePath); err == nil { | |
| return exec.Command(chromePath, "--incognito", url) | |
| } | |
| return exec.Command("open", "-na", "Google Chrome", "--args", "--incognito", url) | |
| case "firefox": | |
| return exec.Command("open", "-na", "Firefox", "--args", "--private-window", url) | |
| case "safari": | |
| // Safari doesn't have CLI incognito, try AppleScript | |
| return tryAppleScriptSafariPrivate(url) | |
| case "brave": | |
| return exec.Command("open", "-na", "Brave Browser", "--args", "--incognito", url) | |
| case "edge": | |
| return exec.Command("open", "-na", "Microsoft Edge", "--args", "--inprivate", url) | |
| } | |
| return nil | |
| } | |
| // tryAppleScriptSafariPrivate attempts to open Safari in private browsing mode using AppleScript. | |
| func tryAppleScriptSafariPrivate(url string) *exec.Cmd { | |
| // AppleScript to open a new private window in Safari | |
| script := fmt.Sprintf(` | |
| tell application "Safari" | |
| activate | |
| tell application "System Events" | |
| keystroke "n" using {command down, shift down} | |
| delay 0.5 | |
| end tell | |
| set URL of document 1 to "%s" | |
| end tell | |
| `, url) | |
| cmd := exec.Command("osascript", "-e", script) | |
| // Test if this approach works by checking if Safari is available | |
| if _, err := exec.LookPath("/Applications/Safari.app/Contents/MacOS/Safari"); err != nil { | |
| log.Debug("Safari not found, AppleScript private window not available") | |
| return nil | |
| } | |
| log.Debug("Attempting Safari private window via AppleScript") | |
| return cmd | |
| } | |
| // tryDefaultBrowserWindows detects the default browser on Windows via registry. | |
| func tryDefaultBrowserWindows(url string) *exec.Cmd { | |
| // Query registry for default browser | |
| out, err := exec.Command("reg", "query", | |
| `HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice`, | |
| "/v", "ProgId").Output() | |
| if err != nil { | |
| return nil | |
| } | |
| output := string(out) | |
| var browserName string | |
| // Map ProgId to browser name | |
| if strings.Contains(output, "ChromeHTML") { | |
| browserName = "chrome" | |
| } else if strings.Contains(output, "FirefoxURL") { | |
| browserName = "firefox" | |
| } else if strings.Contains(output, "MSEdgeHTM") { | |
| browserName = "edge" | |
| } else if strings.Contains(output, "BraveHTML") { | |
| browserName = "brave" | |
| } | |
| return createWindowsIncognitoCmd(browserName, url) | |
| } | |
| // createWindowsIncognitoCmd creates the appropriate incognito command for Windows browsers. | |
| func createWindowsIncognitoCmd(browserName, url string) *exec.Cmd { | |
| switch browserName { | |
| case "chrome": | |
| paths := []string{ | |
| "chrome", | |
| `C:\Program Files\Google\Chrome\Application\chrome.exe`, | |
| `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, | |
| } | |
| for _, p := range paths { | |
| if _, err := exec.LookPath(p); err == nil { | |
| return exec.Command(p, "--incognito", url) | |
| } | |
| } | |
| case "firefox": | |
| if path, err := exec.LookPath("firefox"); err == nil { | |
| return exec.Command(path, "--private-window", url) | |
| } | |
| case "edge": | |
| paths := []string{ | |
| "msedge", | |
| `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, | |
| `C:\Program Files\Microsoft\Edge\Application\msedge.exe`, | |
| } | |
| for _, p := range paths { | |
| if _, err := exec.LookPath(p); err == nil { | |
| return exec.Command(p, "--inprivate", url) | |
| } | |
| } | |
| case "brave": | |
| paths := []string{ | |
| `C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe`, | |
| `C:\Program Files (x86)\BraveSoftware\Brave-Browser\Application\brave.exe`, | |
| } | |
| for _, p := range paths { | |
| if _, err := exec.LookPath(p); err == nil { | |
| return exec.Command(p, "--incognito", url) | |
| } | |
| } | |
| } | |
| return nil | |
| } | |
| // tryDefaultBrowserLinux detects the default browser on Linux using xdg-settings. | |
| func tryDefaultBrowserLinux(url string) *exec.Cmd { | |
| out, err := exec.Command("xdg-settings", "get", "default-web-browser").Output() | |
| if err != nil { | |
| return nil | |
| } | |
| desktop := string(out) | |
| var browserName string | |
| // Map .desktop file to browser name | |
| if strings.Contains(desktop, "google-chrome") || strings.Contains(desktop, "chrome") { | |
| browserName = "chrome" | |
| } else if strings.Contains(desktop, "firefox") { | |
| browserName = "firefox" | |
| } else if strings.Contains(desktop, "chromium") { | |
| browserName = "chromium" | |
| } else if strings.Contains(desktop, "brave") { | |
| browserName = "brave" | |
| } else if strings.Contains(desktop, "microsoft-edge") || strings.Contains(desktop, "msedge") { | |
| browserName = "edge" | |
| } | |
| return createLinuxIncognitoCmd(browserName, url) | |
| } | |
| // createLinuxIncognitoCmd creates the appropriate incognito command for Linux browsers. | |
| func createLinuxIncognitoCmd(browserName, url string) *exec.Cmd { | |
| switch browserName { | |
| case "chrome": | |
| paths := []string{"google-chrome", "google-chrome-stable"} | |
| for _, p := range paths { | |
| if path, err := exec.LookPath(p); err == nil { | |
| return exec.Command(path, "--incognito", url) | |
| } | |
| } | |
| case "firefox": | |
| paths := []string{"firefox", "firefox-esr"} | |
| for _, p := range paths { | |
| if path, err := exec.LookPath(p); err == nil { | |
| return exec.Command(path, "--private-window", url) | |
| } | |
| } | |
| case "chromium": | |
| paths := []string{"chromium", "chromium-browser"} | |
| for _, p := range paths { | |
| if path, err := exec.LookPath(p); err == nil { | |
| return exec.Command(path, "--incognito", url) | |
| } | |
| } | |
| case "brave": | |
| if path, err := exec.LookPath("brave-browser"); err == nil { | |
| return exec.Command(path, "--incognito", url) | |
| } | |
| case "edge": | |
| if path, err := exec.LookPath("microsoft-edge"); err == nil { | |
| return exec.Command(path, "--inprivate", url) | |
| } | |
| } | |
| return nil | |
| } | |
| // tryFallbackBrowsersIncognito tries a chain of known browsers as fallback. | |
| func tryFallbackBrowsersIncognito(url string) *exec.Cmd { | |
| switch runtime.GOOS { | |
| case "darwin": | |
| return tryFallbackBrowsersMacOS(url) | |
| case "windows": | |
| return tryFallbackBrowsersWindows(url) | |
| case "linux": | |
| return tryFallbackBrowsersLinuxChain(url) | |
| } | |
| return nil | |
| } | |
| // tryFallbackBrowsersMacOS tries known browsers on macOS. | |
| func tryFallbackBrowsersMacOS(url string) *exec.Cmd { | |
| // Try Chrome | |
| chromePath := "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" | |
| if _, err := exec.LookPath(chromePath); err == nil { | |
| return exec.Command(chromePath, "--incognito", url) | |
| } | |
| // Try Firefox | |
| if _, err := exec.LookPath("/Applications/Firefox.app/Contents/MacOS/firefox"); err == nil { | |
| return exec.Command("open", "-na", "Firefox", "--args", "--private-window", url) | |
| } | |
| // Try Brave | |
| if _, err := exec.LookPath("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"); err == nil { | |
| return exec.Command("open", "-na", "Brave Browser", "--args", "--incognito", url) | |
| } | |
| // Try Edge | |
| if _, err := exec.LookPath("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"); err == nil { | |
| return exec.Command("open", "-na", "Microsoft Edge", "--args", "--inprivate", url) | |
| } | |
| // Last resort: try Safari with AppleScript | |
| if cmd := tryAppleScriptSafariPrivate(url); cmd != nil { | |
| log.Info("Using Safari with AppleScript for private browsing (may require accessibility permissions)") | |
| return cmd | |
| } | |
| return nil | |
| } | |
| // tryFallbackBrowsersWindows tries known browsers on Windows. | |
| func tryFallbackBrowsersWindows(url string) *exec.Cmd { | |
| // Chrome | |
| chromePaths := []string{ | |
| "chrome", | |
| `C:\Program Files\Google\Chrome\Application\chrome.exe`, | |
| `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, | |
| } | |
| for _, p := range chromePaths { | |
| if _, err := exec.LookPath(p); err == nil { | |
| return exec.Command(p, "--incognito", url) | |
| } | |
| } | |
| // Firefox | |
| if path, err := exec.LookPath("firefox"); err == nil { | |
| return exec.Command(path, "--private-window", url) | |
| } | |
| // Edge (usually available on Windows 10+) | |
| edgePaths := []string{ | |
| "msedge", | |
| `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, | |
| `C:\Program Files\Microsoft\Edge\Application\msedge.exe`, | |
| } | |
| for _, p := range edgePaths { | |
| if _, err := exec.LookPath(p); err == nil { | |
| return exec.Command(p, "--inprivate", url) | |
| } | |
| } | |
| return nil | |
| } | |
| // tryFallbackBrowsersLinuxChain tries known browsers on Linux. | |
| func tryFallbackBrowsersLinuxChain(url string) *exec.Cmd { | |
| type browserConfig struct { | |
| name string | |
| flag string | |
| } | |
| browsers := []browserConfig{ | |
| {"google-chrome", "--incognito"}, | |
| {"google-chrome-stable", "--incognito"}, | |
| {"chromium", "--incognito"}, | |
| {"chromium-browser", "--incognito"}, | |
| {"firefox", "--private-window"}, | |
| {"firefox-esr", "--private-window"}, | |
| {"brave-browser", "--incognito"}, | |
| {"microsoft-edge", "--inprivate"}, | |
| } | |
| for _, b := range browsers { | |
| if path, err := exec.LookPath(b.name); err == nil { | |
| return exec.Command(path, b.flag, url) | |
| } | |
| } | |
| return nil | |
| } | |
| // IsAvailable checks if the system has a command available to open a web browser. | |
| // It verifies the presence of necessary commands for the current operating system. | |
| // | |
| // Returns: | |
| // - true if a browser can be opened, false otherwise. | |
| func IsAvailable() bool { | |
| // Check platform-specific commands | |
| switch runtime.GOOS { | |
| case "darwin": | |
| _, err := exec.LookPath("open") | |
| return err == nil | |
| case "windows": | |
| _, err := exec.LookPath("rundll32") | |
| return err == nil | |
| case "linux": | |
| browsers := []string{"xdg-open", "x-www-browser", "www-browser", "firefox", "chromium", "google-chrome"} | |
| for _, browser := range browsers { | |
| if _, err := exec.LookPath(browser); err == nil { | |
| return true | |
| } | |
| } | |
| return false | |
| default: | |
| return false | |
| } | |
| } | |
| // GetPlatformInfo returns a map containing details about the current platform's | |
| // browser opening capabilities, including the OS, architecture, and available commands. | |
| // | |
| // Returns: | |
| // - A map with platform-specific browser support information. | |
| func GetPlatformInfo() map[string]interface{} { | |
| info := map[string]interface{}{ | |
| "os": runtime.GOOS, | |
| "arch": runtime.GOARCH, | |
| "available": IsAvailable(), | |
| } | |
| switch runtime.GOOS { | |
| case "darwin": | |
| info["default_command"] = "open" | |
| case "windows": | |
| info["default_command"] = "rundll32" | |
| case "linux": | |
| browsers := []string{"xdg-open", "x-www-browser", "www-browser", "firefox", "chromium", "google-chrome"} | |
| var availableBrowsers []string | |
| for _, browser := range browsers { | |
| if _, err := exec.LookPath(browser); err == nil { | |
| availableBrowsers = append(availableBrowsers, browser) | |
| } | |
| } | |
| info["available_browsers"] = availableBrowsers | |
| if len(availableBrowsers) > 0 { | |
| info["default_command"] = availableBrowsers[0] | |
| } | |
| } | |
| return info | |
| } | |