1package cli
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "unicode"
10
11 "github.com/bnema/dumber/internal/config"
12 "github.com/spf13/cobra"
13)
14
15// toTitle capitalizes the first letter of the string
16func toTitle(s string) string {
17 if len(s) == 0 {
18 return s
19 }
20 runes := []rune(s)
21 runes[0] = unicode.ToUpper(runes[0])
22 return string(runes)
23}
24
25// PurgeFlags holds all the purge command flags
26type PurgeFlags struct {
27 Database bool
28 HistoryCache bool
29 BrowserCache bool
30 BrowserData bool
31 State bool
32 Config bool
33 All bool
34 Force bool
35}
36
37// NewPurgeCmd creates the purge command
38func NewPurgeCmd() *cobra.Command {
39 var flags PurgeFlags
40
41 cmd := &cobra.Command{
42 Use: "purge",
43 Short: "Purge dumber data and cache files",
44 Long: `Purge various dumber data and cache files. By default, purges everything.
45
46Available purge targets:
47 --database, -d Purge the SQLite database (history, shortcuts, zoom levels)
48 --history-cache, -H Purge dmenu fuzzy search cache for history
49 --browser-cache, -c Purge WebKit browser cache (cached images, files, etc.)
50 --browser-data, -b Purge WebKit browser data (cookies, localStorage, sessionStorage)
51 --state, -s Purge all state data (includes database and caches)
52 --config Purge configuration files
53 --all, -a Purge everything (default if no specific flags are provided)
54
55Use --force to skip the confirmation prompt.
56
57Examples:
58 dumber purge # Purge everything (with confirmation)
59 dumber purge --force # Purge everything (no confirmation)
60 dumber purge -d -H -c # Purge database and both caches
61 dumber purge --browser-data -f # Force purge browser data only`,
62 RunE: func(_ *cobra.Command, _ []string) error {
63 // Initialize CLI to get config paths
64 cli, err := NewCLI()
65 if err != nil {
66 return fmt.Errorf("failed to initialize CLI: %w", err)
67 }
68 defer func() {
69 if closeErr := cli.Close(); closeErr != nil {
70 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)
71 }
72 }()
73
74 return executePurge(flags)
75 },
76 }
77
78 // Add flags
79 cmd.Flags().BoolVarP(&flags.Database, "database", "d", false, "Purge the SQLite database")
80 cmd.Flags().BoolVarP(&flags.HistoryCache, "history-cache", "H", false, "Purge dmenu fuzzy search cache")
81 cmd.Flags().BoolVarP(&flags.BrowserCache, "browser-cache", "c", false, "Purge WebKit browser cache")
82 cmd.Flags().BoolVarP(&flags.BrowserData, "browser-data", "b", false, "Purge WebKit browser data (cookies, localStorage)")
83 cmd.Flags().BoolVarP(&flags.State, "state", "s", false, "Purge all state data")
84 cmd.Flags().BoolVar(&flags.Config, "config", false, "Purge configuration files")
85 cmd.Flags().BoolVarP(&flags.All, "all", "a", false, "Purge everything")
86 cmd.Flags().BoolVarP(&flags.Force, "force", "f", false, "Skip confirmation prompt")
87
88 return cmd
89}
90
91// executePurge handles the purge logic
92func executePurge(flags PurgeFlags) error {
93 // Determine what to purge
94 purgeItems := determinePurgeItems(flags)
95 if len(purgeItems) == 0 {
96 fmt.Println("Nothing to purge.")
97 return nil
98 }
99
100 // Get file paths for each item
101 paths, err := getPurgePaths(purgeItems)
102 if err != nil {
103 return fmt.Errorf("failed to get purge paths: %w", err)
104 }
105
106 // Show confirmation unless --force is used
107 if !flags.Force {
108 if !confirmPurge(purgeItems, paths) {
109 fmt.Println("Purge cancelled.")
110 return nil
111 }
112 }
113
114 // Execute the purge
115 return performPurge(purgeItems, paths)
116}
117
118// determinePurgeItems determines what should be purged based on flags
119func determinePurgeItems(flags PurgeFlags) []string {
120 var items []string
121
122 // If no specific flags are set, or --all is set, purge everything
123 if flags.All || (!flags.Database && !flags.HistoryCache && !flags.BrowserCache && !flags.BrowserData && !flags.State && !flags.Config) {
124 return []string{"database", "history-cache", "browser-cache", "browser-data", "config"}
125 }
126
127 // Add items based on flags
128 if flags.Database {
129 items = append(items, "database")
130 }
131 if flags.HistoryCache {
132 items = append(items, "history-cache")
133 }
134 if flags.BrowserCache {
135 items = append(items, "browser-cache")
136 }
137 if flags.BrowserData {
138 items = append(items, "browser-data")
139 }
140 if flags.State {
141 // State includes database and both caches
142 items = append(items, "database", "history-cache", "browser-cache")
143 }
144 if flags.Config {
145 items = append(items, "config")
146 }
147
148 // Remove duplicates
149 seen := make(map[string]bool)
150 var result []string
151 for _, item := range items {
152 if !seen[item] {
153 seen[item] = true
154 result = append(result, item)
155 }
156 }
157
158 return result
159}
160
161// getPurgePaths gets the file/directory paths for each purge item
162func getPurgePaths(items []string) (map[string][]string, error) {
163 paths := make(map[string][]string)
164
165 for _, item := range items {
166 switch item {
167 case "database":
168 dbPath, err := config.GetDatabaseFile()
169 if err != nil {
170 return nil, fmt.Errorf("failed to get database path: %w", err)
171 }
172 paths[item] = []string{dbPath}
173
174 case "history-cache":
175 stateDir, err := config.GetStateDir()
176 if err != nil {
177 return nil, fmt.Errorf("failed to get state directory: %w", err)
178 }
179 dmenuCache := filepath.Join(stateDir, "dmenu_fuzzy_cache.bin")
180 paths[item] = []string{dmenuCache}
181
182 case "browser-cache":
183 stateDir, err := config.GetStateDir()
184 if err != nil {
185 return nil, fmt.Errorf("failed to get state directory: %w", err)
186 }
187 webkitCache := filepath.Join(stateDir, "webkit-cache")
188 paths[item] = []string{webkitCache}
189
190 case "browser-data":
191 dataDir, err := config.GetDataDir()
192 if err != nil {
193 return nil, fmt.Errorf("failed to get data directory: %w", err)
194 }
195 webkitData := filepath.Join(dataDir, "webkit")
196 paths[item] = []string{webkitData}
197
198 case "config":
199 configFile, err := config.GetConfigFile()
200 if err != nil {
201 return nil, fmt.Errorf("failed to get config file path: %w", err)
202 }
203 paths[item] = []string{configFile}
204 }
205 }
206
207 return paths, nil
208}
209
210// confirmPurge shows a confirmation prompt and returns true if user confirms
211func confirmPurge(items []string, paths map[string][]string) bool {
212 fmt.Printf("This will delete the following:\n\n")
213
214 for _, item := range items {
215 fmt.Printf("• %s:\n", toTitle(item))
216 for _, path := range paths[item] {
217 // Check if path exists
218 if _, err := os.Stat(path); err == nil {
219 fmt.Printf(" - %s\n", path)
220 } else {
221 fmt.Printf(" - %s (not found)\n", path)
222 }
223 }
224 }
225
226 fmt.Printf("\nAre you sure you want to continue? (y/N): ")
227
228 scanner := bufio.NewScanner(os.Stdin)
229 if scanner.Scan() {
230 response := strings.TrimSpace(strings.ToLower(scanner.Text()))
231 return response == "y" || response == "yes"
232 }
233
234 return false
235}
236
237// performPurge executes the actual deletion
238func performPurge(items []string, paths map[string][]string) error {
239 var errors []string
240
241 for _, item := range items {
242 fmt.Printf("Purging %s...\n", item)
243
244 for _, path := range paths[item] {
245 if err := deletePath(path); err != nil {
246 errors = append(errors, fmt.Sprintf("%s: %v", path, err))
247 fmt.Printf(" ✗ Failed to delete %s: %v\n", path, err)
248 } else {
249 fmt.Printf(" ✓ Deleted %s\n", path)
250 }
251 }
252 }
253
254 if len(errors) > 0 {
255 return fmt.Errorf("some items could not be deleted:\n%s", strings.Join(errors, "\n"))
256 }
257
258 fmt.Println("\nPurge completed successfully!")
259 return nil
260}
261
262// deletePath deletes a file or directory
263func deletePath(path string) error {
264 // Check if path exists
265 info, err := os.Stat(path)
266 if os.IsNotExist(err) {
267 // Path doesn't exist, consider it successful
268 return nil
269 }
270 if err != nil {
271 return fmt.Errorf("failed to check path: %w", err)
272 }
273
274 // Delete based on type
275 if info.IsDir() {
276 return os.RemoveAll(path)
277 }
278 return os.Remove(path)
279}