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}