v0.6.0 5c50dd3

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	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}

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