v0.15.0 9006957

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

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