v0.14.0 76d1892

browse.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close