v0.7.0 31c7fae

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

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