v0.7.0 31c7fae

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/cache"
13	"github.com/bnema/dumber/internal/config"
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	var sqlTitle sql.NullString
94	if title != "" {
95		sqlTitle = sql.NullString{String: title, Valid: true}
96	}
97
98	return cli.Queries.AddOrUpdateHistory(ctx, url, sqlTitle)
99}
100
101// openURL opens a URL using the configured browser
102func openURL(url string) error { //nolint:unused // Public API function
103	return openURLWithConfig(url, nil)
104}
105
106// openURLWithConfig opens a URL using our built-in browser
107func openURLWithConfig(url string, cfg *config.Config) error {
108	// Get the path to our own executable
109	executable, err := os.Executable()
110	if err != nil {
111		return fmt.Errorf("failed to get executable path: %w", err)
112	}
113
114	// Launch our own browser in browse mode with the URL directly
115	cmd := exec.Command(executable, "browse", url)
116
117	// The subprocess will read the same config file and apply the proper precedence:
118	// 1. Config file defaults
119	// 2. Environment variables (override config)
120	// 3. Command line flags (override both)
121	//
122	// We don't convert config to env vars here because that would break precedence.
123	// The cfg parameter ensures we use the same config source in both processes.
124
125	// Start the browser in detached mode
126	if err := cmd.Start(); err != nil {
127		return fmt.Errorf("failed to open URL in built-in browser: %w", err)
128	}
129
130	// Don't wait for the browser process to exit
131	go func() {
132		_ = cmd.Wait()
133	}()
134
135	fmt.Printf("Opening: %s (using built-in browser)\n", url)
136	return nil
137}
138
139// updateFavicon fetches and stores favicon URL for a given page URL
140func updateFavicon(ctx context.Context, cli *CLI, pageURL string) {
141	parsedURL, err := url.Parse(pageURL)
142	if err != nil {
143		return // Silently fail for invalid URLs
144	}
145
146	// Skip favicon update for localhost, file://, or special schemes
147	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
148		return
149	}
150	if strings.Contains(parsedURL.Host, "localhost") || strings.Contains(parsedURL.Host, "127.0.0.1") {
151		return
152	}
153
154	// Standard favicon location
155	faviconURL := fmt.Sprintf("%s://%s/favicon.ico", parsedURL.Scheme, parsedURL.Host)
156
157	// Update in database using the new sqlc-generated method
158	faviconNullString := sql.NullString{String: faviconURL, Valid: true}
159	if err := cli.Queries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil {
160		// Silently fail - favicon is not critical
161		return
162	}
163
164	// Also cache the favicon for dmenu use
165	if faviconCache, err := cache.NewFaviconCache(); err == nil {
166		faviconCache.CacheAsync(faviconURL)
167	}
168}

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