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}