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}