v0.1 bf7c821

root.go RawHistoryFolder

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

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