v0.4.0 6203a22

purge.go RawHistoryFolder

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

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