v0.12.0 d035193

root.go RawHistoryFolder

1// Package cli provides the command-line interface for dumber.
2package cli
3
4import (
5	"database/sql"
6	"fmt"
7	"log"
8	"os"
9	"path/filepath"
10
11	"github.com/bnema/dumber/internal/cache"
12	"github.com/bnema/dumber/internal/config"
13	"github.com/bnema/dumber/internal/db"
14	"github.com/bnema/dumber/internal/services"
15
16	_ "github.com/ncruces/go-sqlite3/driver" // SQLite driver for database/sql
17	_ "github.com/ncruces/go-sqlite3/embed"  // Embed SQLite for cross-platform compatibility
18	"github.com/spf13/cobra"
19)
20
21// File permission constants
22const (
23	dirPerm = 0755 // Standard directory permissions (rwxr-xr-x)
24)
25
26// CLI holds the database connection, queries, and configuration for the CLI commands
27type CLI struct {
28	DB            *sql.DB
29	Queries       *db.Queries
30	Config        *config.Config
31	ParserService *services.ParserService
32}
33
34// NewCLI creates a new CLI instance with database connection and configuration
35func NewCLI() (*CLI, error) {
36	// Get configuration
37	cfg := config.Get()
38
39	// Use configured database path
40	dbPath := cfg.Database.Path
41	if dbPath == "" {
42		// Fallback to default path if not configured
43		var err error
44		dbPath, err = config.GetDatabaseFile()
45		if err != nil {
46			return nil, fmt.Errorf("failed to get database path: %w", err)
47		}
48	}
49
50	// Ensure database directory exists
51	if err := os.MkdirAll(filepath.Dir(dbPath), dirPerm); err != nil {
52		return nil, fmt.Errorf("failed to create database directory: %w", err)
53	}
54
55	// Open database connection
56	database, err := sql.Open("sqlite3", dbPath)
57	if err != nil {
58		return nil, fmt.Errorf("failed to open database: %w", err)
59	}
60
61	// Test the connection
62	if err := database.Ping(); err != nil {
63		if err := database.Close(); err != nil {
64			log.Printf("Warning: failed to close database: %v", err)
65		}
66		return nil, fmt.Errorf("failed to connect to database: %w", err)
67	}
68
69	// Initialize database schema with configuration-based shortcuts
70	if err := initializeDatabase(database, cfg); err != nil {
71		if err := database.Close(); err != nil {
72			log.Printf("Warning: failed to close database: %v", err)
73		}
74		return nil, fmt.Errorf("failed to initialize database: %w", err)
75	}
76
77	queries := db.New(database)
78	parserService := services.NewParserService(cfg, queries)
79
80	// Clean old cached favicons on startup (async, non-blocking)
81	go func() {
82		if faviconCache, err := cache.NewFaviconCache(); err == nil {
83			faviconCache.CleanOld()
84		}
85	}()
86
87	return &CLI{
88		DB:            database,
89		Queries:       queries,
90		Config:        cfg,
91		ParserService: parserService,
92	}, nil
93}
94
95// Close closes the database connection
96func (c *CLI) Close() error {
97	if c.DB != nil {
98		return c.DB.Close()
99	}
100	return nil
101}
102
103// NewRootCmd creates the root command for dumber
104func NewRootCmd(version, commit, buildDate string) *cobra.Command {
105	rootCmd := &cobra.Command{
106		Use:   "dumber [url]",
107		Short: "A dumb browser for Wayland window managers",
108		Long:  `A fast, simple browser with rofi/dmenu integration for sway and hyprland window managers.`,
109		Args:  cobra.MaximumNArgs(1),
110		RunE: func(cmd *cobra.Command, args []string) error {
111			// Initialize CLI
112			cli, err := NewCLI()
113			if err != nil {
114				return fmt.Errorf("failed to initialize CLI: %w", err)
115			}
116			defer func() {
117				if closeErr := cli.Close(); closeErr != nil {
118					fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)
119				}
120			}()
121
122			// If URL provided, browse it directly
123			if len(args) > 0 {
124				return browse(cli, args[0])
125			}
126
127			// Otherwise show help
128			return cmd.Help()
129		},
130	}
131
132	// Add dmenu flag for launcher integration
133	rootCmd.Flags().Bool("dmenu", false, "Run in dmenu mode for launcher integration")
134
135	// Version command
136	versionCmd := &cobra.Command{
137		Use:   "version",
138		Short: "Print version information",
139		Run: func(_ *cobra.Command, _ []string) {
140			fmt.Printf("dumber %s\n", version)
141			fmt.Printf("commit: %s\n", commit)
142			fmt.Printf("built: %s\n", buildDate)
143		},
144	}
145
146	// Init command (enhanced)
147	initCmd := &cobra.Command{
148		Use:   "init",
149		Short: "Initialize dumber database and configuration",
150		RunE: func(_ *cobra.Command, _ []string) error {
151			cli, err := NewCLI()
152			if err != nil {
153				return fmt.Errorf("failed to initialize CLI: %w", err)
154			}
155			defer func() {
156				if closeErr := cli.Close(); closeErr != nil {
157					fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)
158				}
159			}()
160
161			fmt.Printf("dumber %s - Initialization complete!\n", version)
162			fmt.Println("Database initialized at:", cli.Config.Database.Path)
163
164			// Show XDG directories
165			xdgDirs, err := config.GetXDGDirs()
166			if err == nil {
167				fmt.Println("Configuration directories:")
168				fmt.Printf("- Config: %s\n", xdgDirs.ConfigHome)
169				fmt.Printf("- Data: %s\n", xdgDirs.DataHome)
170				fmt.Printf("- State: %s\n", xdgDirs.StateHome)
171			}
172
173			// Show configured shortcuts
174			fmt.Println("Configured shortcuts:")
175			for shortcut, cfg := range cli.Config.SearchShortcuts {
176				fmt.Printf("- %s:%s -> %s\n", shortcut, "query", cfg.Description)
177			}
178			return nil
179		},
180	}
181
182	// Add subcommands
183	rootCmd.AddCommand(versionCmd)
184	rootCmd.AddCommand(initCmd)
185	rootCmd.AddCommand(NewBrowseCmd())
186	rootCmd.AddCommand(NewDmenuCmd())
187	rootCmd.AddCommand(NewHistoryCmd())
188	rootCmd.AddCommand(NewPurgeCmd())
189	rootCmd.AddCommand(NewLogsCmd())
190	rootCmd.AddCommand(NewConfigCmd())
191
192	return rootCmd
193}
194
195// initializeDatabase ensures configured shortcuts are inserted
196// Migrations are run automatically in db.InitDB()
197func initializeDatabase(database *sql.DB, cfg *config.Config) error {
198	// Insert configured shortcuts (these are additive, won't override existing)
199	for shortcut, shortcutCfg := range cfg.SearchShortcuts {
200		_, err := database.Exec(
201			"INSERT OR IGNORE INTO shortcuts (shortcut, url_template, description) VALUES (?, ?, ?)",
202			shortcut, shortcutCfg.URL, shortcutCfg.Description,
203		)
204		if err != nil {
205			return fmt.Errorf("failed to insert shortcut %s: %w", shortcut, err)
206		}
207	}
208
209	return nil
210}

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