v0.14.0 76d1892

purge.go RawHistoryFolder

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

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