1package cli
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "net/url"
8 "os"
9 "os/exec"
10 "strings"
11
12 "github.com/bnema/dumber/internal/config"
13
14 "github.com/spf13/cobra"
15)
16
17// NewBrowseCmd creates the browse command
18func NewBrowseCmd() *cobra.Command {
19 var renderingMode string
20
21 cmd := &cobra.Command{
22 Use: "browse <url|query>",
23 Short: "Browse a URL or search query",
24 Long: `Browse a URL directly or use search shortcuts like:
25 g:golang -> Google search for "golang"
26 gh:cobra -> GitHub search for "cobra"
27 yt:tutorials -> YouTube search for "tutorials"
28 w:go language -> Wikipedia search for "go language"
29
30Direct URLs are also supported:
31 https://example.com
32 example.com (automatically adds https://)`,
33 Args: cobra.ExactArgs(1),
34 RunE: func(_ *cobra.Command, args []string) error {
35 // Propagate rendering mode to GUI process via env var
36 if renderingMode != "" {
37 rm := strings.ToLower(renderingMode)
38 switch rm {
39 case "auto", "gpu", "cpu":
40 _ = os.Setenv("DUMBER_RENDERING_MODE", rm)
41 default:
42 return fmt.Errorf("invalid --rendering-mode: %s (expected auto|gpu|cpu)", renderingMode)
43 }
44 }
45 cli, err := NewCLI()
46 if err != nil {
47 return fmt.Errorf("failed to initialize CLI: %w", err)
48 }
49 defer func() {
50 if closeErr := cli.Close(); closeErr != nil {
51 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)
52 }
53 }()
54
55 return browse(cli, args[0])
56 },
57 }
58
59 // Flags
60 cmd.Flags().StringVar(&renderingMode, "rendering-mode", "", "Rendering mode: auto|gpu|cpu")
61
62 return cmd
63}
64
65// browse handles the core browsing logic
66func browse(cli *CLI, input string) error {
67 ctx := context.Background()
68
69 // Parse the input using parser service
70 result, err := cli.ParserService.ParseInput(ctx, input)
71 if err != nil {
72 return fmt.Errorf("failed to parse input: %w", err)
73 }
74
75 finalURL := result.URL
76
77 // Record in history
78 if err := recordVisit(ctx, cli, finalURL, ""); err != nil {
79 // Don't fail the browse operation if history recording fails
80 fmt.Fprintf(os.Stderr, "Warning: failed to record history: %v\n", err)
81 }
82
83 // Update favicon asynchronously (non-blocking)
84 go updateFavicon(ctx, cli, finalURL)
85
86 // Open URL using configuration
87 return openURLWithConfig(finalURL, cli.Config)
88}
89
90// recordVisit adds or updates a URL visit in the history
91func recordVisit(ctx context.Context, cli *CLI, url, title string) error {
92 var sqlTitle sql.NullString
93 if title != "" {
94 sqlTitle = sql.NullString{String: title, Valid: true}
95 }
96
97 return cli.Queries.AddOrUpdateHistory(ctx, url, sqlTitle)
98}
99
100// openURL opens a URL using the configured browser
101func openURL(url string) error { //nolint:unused // Public API function
102 return openURLWithConfig(url, nil)
103}
104
105// openURLWithConfig opens a URL using our built-in browser
106func openURLWithConfig(url string, cfg *config.Config) error {
107 // Get the path to our own executable
108 executable, err := os.Executable()
109 if err != nil {
110 return fmt.Errorf("failed to get executable path: %w", err)
111 }
112
113 // Launch our own browser in browse mode with the URL directly
114 cmd := exec.Command(executable, "browse", url)
115
116 // The subprocess will read the same config file and apply the proper precedence:
117 // 1. Config file defaults
118 // 2. Environment variables (override config)
119 // 3. Command line flags (override both)
120 //
121 // We don't convert config to env vars here because that would break precedence.
122 // The cfg parameter ensures we use the same config source in both processes.
123
124 // Start the browser in detached mode
125 if err := cmd.Start(); err != nil {
126 return fmt.Errorf("failed to open URL in built-in browser: %w", err)
127 }
128
129 // Don't wait for the browser process to exit
130 go func() {
131 _ = cmd.Wait()
132 }()
133
134 fmt.Printf("Opening: %s (using built-in browser)\n", url)
135 return nil
136}
137
138// updateFavicon is now deprecated in favor of WebKit's native favicon API
139// When using the built-in browser, WebKit automatically handles favicon detection
140// This function is kept for CLI compatibility but may use fallback behavior
141func updateFavicon(ctx context.Context, cli *CLI, pageURL string) {
142 // For CLI usage (non-GUI), we still use the fallback method
143 // The GUI browser handles favicons through WebKit signals
144 parsedURL, err := url.Parse(pageURL)
145 if err != nil {
146 return // Silently fail for invalid URLs
147 }
148
149 // Skip favicon update for localhost, file://, or special schemes
150 if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
151 return
152 }
153 if strings.Contains(parsedURL.Host, "localhost") || strings.Contains(parsedURL.Host, "127.0.0.1") {
154 return
155 }
156
157 // Standard favicon location (fallback)
158 faviconURL := fmt.Sprintf("%s://%s/favicon.ico", parsedURL.Scheme, parsedURL.Host)
159
160 // Update in database using the new sqlc-generated method
161 faviconNullString := sql.NullString{String: faviconURL, Valid: true}
162 if err := cli.Queries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil {
163 return
164 }
165}