detached 0639fb7

feat(cli): add --filters flag to purge command for filter cache management

commit
0639fb7b5691a75ebbc11001595cfcd7295406a5
author
brice · · 2025-12-18 17:58 UTC
committer
brice · · 2025-12-18 21:48 UTC
parent
7505dfa
signature
unsigned
1 file · +88 -1
M internal/cli/cmd/purge.go +88 -1
@@ -18,6 +18,7 @@ var (
1818 purgeForce bool
1919 purgeAll bool
2020 purgeDesktop bool
21+ purgeFilters bool
2122 )
2223
2324 var purgeCmd = &cobra.Command{
@@ -32,7 +33,8 @@ This will delete:
3233 - ~/.cache/dumber/ (cache)
3334
3435 Use --force to skip confirmation prompt.
35-Use --desktop to also remove the desktop file from ~/.local/share/applications/`,
36+Use --desktop to also remove the desktop file from ~/.local/share/applications/
37+Use --filters to only remove content filter cache (for re-downloading filters)`,
3638 RunE: runPurge,
3739 }
3840
@@ -41,6 +43,7 @@ func init() {
4143 purgeCmd.Flags().BoolVarP(&purgeForce, "force", "f", false, "skip confirmation prompt")
4244 purgeCmd.Flags().BoolVar(&purgeAll, "all", true, "remove all directories (default)")
4345 purgeCmd.Flags().BoolVar(&purgeDesktop, "desktop", false, "also remove desktop file")
46+ purgeCmd.Flags().BoolVar(&purgeFilters, "filters", false, "only remove content filter cache")
4447 }
4548
4649 func runPurge(cmd *cobra.Command, args []string) error {
@@ -49,6 +52,11 @@ func runPurge(cmd *cobra.Command, args []string) error {
4952 return fmt.Errorf("get home dir: %w", err)
5053 }
5154
55+ // Handle --filters flag (only purge filter cache)
56+ if purgeFilters {
57+ return runPurgeFilters(home)
58+ }
59+
5260 // Build list of directories to remove
5361 dirs := []struct {
5462 path string
@@ -225,3 +233,82 @@ func formatSize(bytes int64) string {
225233 }
226234 return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
227235 }
236+
237+// runPurgeFilters removes only the content filter cache.
238+func runPurgeFilters(home string) error {
239+ dataDir := getXDGPath("XDG_DATA_HOME", home, ".local/share", "dumber")
240+ filterDirs := []struct {
241+ path string
242+ desc string
243+ }{
244+ {filepath.Join(dataDir, "filters", "json"), "filter JSON cache"},
245+ {filepath.Join(dataDir, "filters", "store"), "compiled filters"},
246+ }
247+
248+ // Check which directories exist
249+ var existing []struct {
250+ path string
251+ desc string
252+ size int64
253+ }
254+
255+ for _, d := range filterDirs {
256+ if info, err := os.Stat(d.path); err == nil && info.IsDir() {
257+ size := getDirSize(d.path)
258+ existing = append(existing, struct {
259+ path string
260+ desc string
261+ size int64
262+ }{d.path, d.desc, size})
263+ }
264+ }
265+
266+ if len(existing) == 0 {
267+ fmt.Println("No filter cache found.")
268+ return nil
269+ }
270+
271+ // Show what will be deleted
272+ fmt.Println("The following filter cache will be removed:")
273+ fmt.Println()
274+ for _, d := range existing {
275+ fmt.Printf(" • %s (%s, %s)\n", d.path, d.desc, formatSize(d.size))
276+ }
277+ fmt.Println()
278+
279+ // Confirm unless --force
280+ if !purgeForce {
281+ fmt.Print("Are you sure? [y/N]: ")
282+ reader := bufio.NewReader(os.Stdin)
283+ response, _ := reader.ReadString('\n')
284+ response = strings.TrimSpace(strings.ToLower(response))
285+
286+ if response != "y" && response != "yes" {
287+ fmt.Println("Aborted.")
288+ return nil
289+ }
290+ }
291+
292+ // Remove directories
293+ var errors []string
294+ for _, d := range existing {
295+ if err := os.RemoveAll(d.path); err != nil {
296+ errors = append(errors, fmt.Sprintf("%s: %v", d.path, err))
297+ } else {
298+ fmt.Printf("✓ Removed %s\n", d.path)
299+ }
300+ }
301+
302+ if len(errors) > 0 {
303+ fmt.Println()
304+ fmt.Println("Errors:")
305+ for _, e := range errors {
306+ fmt.Printf(" ✗ %s\n", e)
307+ }
308+ return fmt.Errorf("failed to remove some items")
309+ }
310+
311+ fmt.Println()
312+ fmt.Println("Filter cache purged. Filters will be re-downloaded on next browser start.")
313+ return nil
314+}
@@ -18,6 +18,7 @@ var (
18 purgeForce bool18 purgeForce bool
19 purgeAll bool19 purgeAll bool
20 purgeDesktop bool20 purgeDesktop bool
21+ purgeFilters bool
21 )22 )
22 23
23 var purgeCmd = &cobra.Command{24 var purgeCmd = &cobra.Command{
@@ -32,7 +33,8 @@ This will delete:
32 - ~/.cache/dumber/ (cache)33 - ~/.cache/dumber/ (cache)
33 34
34 Use --force to skip confirmation prompt.35 Use --force to skip confirmation prompt.
35-Use --desktop to also remove the desktop file from ~/.local/share/applications/`,36+Use --desktop to also remove the desktop file from ~/.local/share/applications/
37+Use --filters to only remove content filter cache (for re-downloading filters)`,
36 RunE: runPurge,38 RunE: runPurge,
37 }39 }
38 40
@@ -41,6 +43,7 @@ func init() {
41 purgeCmd.Flags().BoolVarP(&purgeForce, "force", "f", false, "skip confirmation prompt")43 purgeCmd.Flags().BoolVarP(&purgeForce, "force", "f", false, "skip confirmation prompt")
42 purgeCmd.Flags().BoolVar(&purgeAll, "all", true, "remove all directories (default)")44 purgeCmd.Flags().BoolVar(&purgeAll, "all", true, "remove all directories (default)")
43 purgeCmd.Flags().BoolVar(&purgeDesktop, "desktop", false, "also remove desktop file")45 purgeCmd.Flags().BoolVar(&purgeDesktop, "desktop", false, "also remove desktop file")
46+ purgeCmd.Flags().BoolVar(&purgeFilters, "filters", false, "only remove content filter cache")
44 }47 }
45 48
46 func runPurge(cmd *cobra.Command, args []string) error {49 func runPurge(cmd *cobra.Command, args []string) error {
@@ -49,6 +52,11 @@ func runPurge(cmd *cobra.Command, args []string) error {
49 return fmt.Errorf("get home dir: %w", err)52 return fmt.Errorf("get home dir: %w", err)
50 }53 }
51 54
55+ // Handle --filters flag (only purge filter cache)
56+ if purgeFilters {
57+ return runPurgeFilters(home)
58+ }
59+
52 // Build list of directories to remove60 // Build list of directories to remove
53 dirs := []struct {61 dirs := []struct {
54 path string62 path string
@@ -225,3 +233,82 @@ func formatSize(bytes int64) string {
225 }233 }
226 return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])234 return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
227 }235 }
236+
237+// runPurgeFilters removes only the content filter cache.
238+func runPurgeFilters(home string) error {
239+ dataDir := getXDGPath("XDG_DATA_HOME", home, ".local/share", "dumber")
240+ filterDirs := []struct {
241+ path string
242+ desc string
243+ }{
244+ {filepath.Join(dataDir, "filters", "json"), "filter JSON cache"},
245+ {filepath.Join(dataDir, "filters", "store"), "compiled filters"},
246+ }
247+
248+ // Check which directories exist
249+ var existing []struct {
250+ path string
251+ desc string
252+ size int64
253+ }
254+
255+ for _, d := range filterDirs {
256+ if info, err := os.Stat(d.path); err == nil && info.IsDir() {
257+ size := getDirSize(d.path)
258+ existing = append(existing, struct {
259+ path string
260+ desc string
261+ size int64
262+ }{d.path, d.desc, size})
263+ }
264+ }
265+
266+ if len(existing) == 0 {
267+ fmt.Println("No filter cache found.")
268+ return nil
269+ }
270+
271+ // Show what will be deleted
272+ fmt.Println("The following filter cache will be removed:")
273+ fmt.Println()
274+ for _, d := range existing {
275+ fmt.Printf(" • %s (%s, %s)\n", d.path, d.desc, formatSize(d.size))
276+ }
277+ fmt.Println()
278+
279+ // Confirm unless --force
280+ if !purgeForce {
281+ fmt.Print("Are you sure? [y/N]: ")
282+ reader := bufio.NewReader(os.Stdin)
283+ response, _ := reader.ReadString('\n')
284+ response = strings.TrimSpace(strings.ToLower(response))
285+
286+ if response != "y" && response != "yes" {
287+ fmt.Println("Aborted.")
288+ return nil
289+ }
290+ }
291+
292+ // Remove directories
293+ var errors []string
294+ for _, d := range existing {
295+ if err := os.RemoveAll(d.path); err != nil {
296+ errors = append(errors, fmt.Sprintf("%s: %v", d.path, err))
297+ } else {
298+ fmt.Printf("✓ Removed %s\n", d.path)
299+ }
300+ }
301+
302+ if len(errors) > 0 {
303+ fmt.Println()
304+ fmt.Println("Errors:")
305+ for _, e := range errors {
306+ fmt.Printf(" ✗ %s\n", e)
307+ }
308+ return fmt.Errorf("failed to remove some items")
309+ }
310+
311+ fmt.Println()
312+ fmt.Println("Filter cache purged. Filters will be re-downloaded on next browser start.")
313+ return nil
314+}

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