detached 202ed5a

feat(filtering): add global content filtering toggle in config

Adds ability to completely disable ad blocking via configuration while
maintaining the whitelist system for granular control.

Changes:
- Refactored ContentFilteringWhitelist into ContentFilteringConfig struct
  with separate 'enabled' boolean and 'whitelist' array
- FilterManager now checks config.ContentFiltering.Enabled before applying filters
- Defaults to enabled=true for backward compatibility
- When disabled, no network rules or cosmetic filters are loaded
- Whitelist cache optimization with hash-based change detection to avoid
  rebuilding rules on every filter application

Config structure:
  content_filtering:
    enabled: true
    whitelist:
      - "twitch.tv"
      - "passport.twitch.tv"
      - "gql.twitch.tv"

This allows users to completely bypass ad blocking when needed (e.g., debugging,
site compatibility) without removing the configuration file.
commit
202ed5abb8da138fbf616a6ab9f45e5cb96fe6c4
author
bnema · · 2025-11-17 22:14 UTC
committer
bnema · · 2025-11-17 21:58 UTC
parent
72ad164
signature
unsigned
3 files · +78 -7
M internal/config/config.go +14 -2
@@ -42,8 +42,8 @@ type Config struct {
4242 DefaultZoom float64 `mapstructure:"default_zoom" yaml:"default_zoom"`
4343 // Workspace defines workspace, pane, and tab handling behaviour.
4444 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace"`
45- // ContentFilteringWhitelist domains skip ad blocking (e.g. Twitch breaks with bot detection)
46- ContentFilteringWhitelist []string `mapstructure:"content_filtering_whitelist" yaml:"content_filtering_whitelist"`
45+ // ContentFiltering controls ad blocking and content filtering
46+ ContentFiltering ContentFilteringConfig `mapstructure:"content_filtering" yaml:"content_filtering"`
4747 }
4848
4949 // RenderingMode selects GPU vs CPU rendering.
@@ -175,6 +175,14 @@ type CodecConfig struct {
175175 }
176176
177177 // DebugConfig holds debug and troubleshooting options
178+// ContentFilteringConfig holds content filtering and ad blocking preferences
179+type ContentFilteringConfig struct {
180+ // Enabled controls whether ad blocking is active (default: true)
181+ Enabled bool `mapstructure:"enabled" yaml:"enabled"`
182+ // Whitelist domains that should skip ad blocking (e.g. Twitch breaks with bot detection)
183+ Whitelist []string `mapstructure:"whitelist" yaml:"whitelist"`
184+}
185+
178186 type DebugConfig struct {
179187 // Enable WebKit internal debug logging
180188 EnableWebKitDebug bool `mapstructure:"enable_webkit_debug" yaml:"enable_webkit_debug"`
@@ -744,6 +752,10 @@ func (m *Manager) setDefaults() {
744752 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
745753 m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)
746754 m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale)
755+
756+ // Content filtering
757+ m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled)
758+ m.viper.SetDefault("content_filtering.whitelist", defaults.ContentFiltering.Whitelist)
747759 }
748760
749761 // createDefaultConfig creates a default configuration file.
@@ -42,8 +42,8 @@ type Config struct {
42 DefaultZoom float64 `mapstructure:"default_zoom" yaml:"default_zoom"`42 DefaultZoom float64 `mapstructure:"default_zoom" yaml:"default_zoom"`
43 // Workspace defines workspace, pane, and tab handling behaviour.43 // Workspace defines workspace, pane, and tab handling behaviour.
44 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace"`44 Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace"`
45- // ContentFilteringWhitelist domains skip ad blocking (e.g. Twitch breaks with bot detection)45+ // ContentFiltering controls ad blocking and content filtering
46- ContentFilteringWhitelist []string `mapstructure:"content_filtering_whitelist" yaml:"content_filtering_whitelist"`46+ ContentFiltering ContentFilteringConfig `mapstructure:"content_filtering" yaml:"content_filtering"`
47 }47 }
48 48
49 // RenderingMode selects GPU vs CPU rendering.49 // RenderingMode selects GPU vs CPU rendering.
@@ -175,6 +175,14 @@ type CodecConfig struct {
175 }175 }
176 176
177 // DebugConfig holds debug and troubleshooting options177 // DebugConfig holds debug and troubleshooting options
178+// ContentFilteringConfig holds content filtering and ad blocking preferences
179+type ContentFilteringConfig struct {
180+ // Enabled controls whether ad blocking is active (default: true)
181+ Enabled bool `mapstructure:"enabled" yaml:"enabled"`
182+ // Whitelist domains that should skip ad blocking (e.g. Twitch breaks with bot detection)
183+ Whitelist []string `mapstructure:"whitelist" yaml:"whitelist"`
184+}
185+
178 type DebugConfig struct {186 type DebugConfig struct {
179 // Enable WebKit internal debug logging187 // Enable WebKit internal debug logging
180 EnableWebKitDebug bool `mapstructure:"enable_webkit_debug" yaml:"enable_webkit_debug"`188 EnableWebKitDebug bool `mapstructure:"enable_webkit_debug" yaml:"enable_webkit_debug"`
@@ -744,6 +752,10 @@ func (m *Manager) setDefaults() {
744 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)752 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
745 m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)753 m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)
746 m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale)754 m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale)
755+
756+ // Content filtering
757+ m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled)
758+ m.viper.SetDefault("content_filtering.whitelist", defaults.ContentFiltering.Whitelist)
747 }759 }
748 760
749 // createDefaultConfig creates a default configuration file.761 // createDefaultConfig creates a default configuration file.
M internal/config/defaults.go +8 -5
@@ -129,7 +129,7 @@ func DefaultConfig() *Config {
129129 DisableVP9Hardware: false, // Allow VP9 hardware for high res content
130130 VideoBufferSizeMB: 16, // Larger buffer for AV1 streams
131131 QueueBufferTimeSec: 10, // More buffering time for smooth playback
132- CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", // Chrome UA with AV1 support
132+ CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // WebKitGTK default (same as Epiphany)
133133 AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res
134134 DisableTwitchCodecControl: true, // Disable codec control on Twitch by default (prevents theater/fullscreen freezing)
135135 },
@@ -206,10 +206,13 @@ func DefaultConfig() *Config {
206206 UIScale: defaultUIScale,
207207 },
208208 },
209- ContentFilteringWhitelist: []string{
210- "twitch.tv", // Arkose Labs bot detection breaks with filtering
211- "passport.twitch.tv", // Auth subdomain
212- "gql.twitch.tv", // GraphQL API
209+ ContentFiltering: ContentFilteringConfig{
210+ Enabled: true, // Ad blocking enabled by default
211+ Whitelist: []string{
212+ "twitch.tv", // Arkose Labs bot detection breaks with filtering
213+ "passport.twitch.tv", // Auth subdomain
214+ "gql.twitch.tv", // GraphQL API
215+ },
213216 },
214217 }
215218 }
@@ -129,7 +129,7 @@ func DefaultConfig() *Config {
129 DisableVP9Hardware: false, // Allow VP9 hardware for high res content129 DisableVP9Hardware: false, // Allow VP9 hardware for high res content
130 VideoBufferSizeMB: 16, // Larger buffer for AV1 streams130 VideoBufferSizeMB: 16, // Larger buffer for AV1 streams
131 QueueBufferTimeSec: 10, // More buffering time for smooth playback131 QueueBufferTimeSec: 10, // More buffering time for smooth playback
132- CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", // Chrome UA with AV1 support132+ CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // WebKitGTK default (same as Epiphany)
133 AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res133 AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res
134 DisableTwitchCodecControl: true, // Disable codec control on Twitch by default (prevents theater/fullscreen freezing)134 DisableTwitchCodecControl: true, // Disable codec control on Twitch by default (prevents theater/fullscreen freezing)
135 },135 },
@@ -206,10 +206,13 @@ func DefaultConfig() *Config {
206 UIScale: defaultUIScale,206 UIScale: defaultUIScale,
207 },207 },
208 },208 },
209- ContentFilteringWhitelist: []string{209+ ContentFiltering: ContentFilteringConfig{
210- "twitch.tv", // Arkose Labs bot detection breaks with filtering210+ Enabled: true, // Ad blocking enabled by default
211- "passport.twitch.tv", // Auth subdomain211+ Whitelist: []string{
212- "gql.twitch.tv", // GraphQL API212+ "twitch.tv", // Arkose Labs bot detection breaks with filtering
213+ "passport.twitch.tv", // Auth subdomain
214+ "gql.twitch.tv", // GraphQL API
215+ },
213 },216 },
214 }217 }
215 }218 }
M internal/filtering/manager.go +56 -0
@@ -7,9 +7,11 @@ import (
77 "encoding/json"
88 "fmt"
99 "net/http"
10+ "strings"
1011 "sync"
1112 "time"
1213
14+ "github.com/bnema/dumber/internal/config"
1315 "github.com/bnema/dumber/internal/filtering/converter"
1416 "github.com/bnema/dumber/internal/filtering/cosmetic"
1517 "github.com/bnema/dumber/internal/logging"
@@ -33,6 +35,9 @@ type FilterManager struct {
3335 compileMutex sync.RWMutex
3436 compiled *CompiledFilters
3537 onFiltersReady FilterReadyCallback
38+ // Cached whitelist rules to avoid recomputing on every filter application
39+ cachedWhitelistRules []converter.WebKitRule
40+ cachedWhitelistHash string
3641 }
3742
3843 // FilterStore defines the interface for filter storage operations
@@ -76,6 +81,41 @@ func NewFilterManager(store FilterStore, compiler FilterCompiler) *FilterManager
7681 }
7782 }
7883
84+// updateWhitelistCache rebuilds the cached whitelist rules from config if needed
85+func (fm *FilterManager) updateWhitelistCache(whitelist []string) {
86+ // Create a hash of the whitelist to detect changes
87+ currentHash := strings.Join(whitelist, "|")
88+
89+ // Only rebuild if whitelist changed
90+ if fm.cachedWhitelistHash == currentHash && fm.cachedWhitelistRules != nil {
91+ return
92+ }
93+
94+ // Rebuild cache with pre-allocation
95+ rules := make([]converter.WebKitRule, 0, len(whitelist))
96+ for _, domain := range whitelist {
97+ // Create a regex pattern that matches all URLs containing this domain
98+ // Escape special regex characters in the domain
99+ escapedDomain := strings.ReplaceAll(domain, ".", "\\.")
100+ // Match any URL containing this domain (with protocol and optional path)
101+ urlPattern := ".*" + escapedDomain
102+
103+ rules = append(rules, converter.WebKitRule{
104+ Trigger: converter.Trigger{
105+ URLFilter: urlPattern,
106+ },
107+ Action: converter.Action{
108+ Type: converter.ActionTypeIgnorePreviousRules,
109+ },
110+ })
111+ logging.Info(fmt.Sprintf("[filtering] Created whitelist rule: %s → pattern: %s", domain, urlPattern))
112+ }
113+
114+ fm.cachedWhitelistRules = rules
115+ fm.cachedWhitelistHash = currentHash
116+ logging.Info(fmt.Sprintf("[filtering] Whitelist cache updated with %d domains", len(whitelist)))
117+}
118+
79119 // SetFiltersReadyCallback sets a callback to be called when filters are loaded/updated
80120 func (fm *FilterManager) SetFiltersReadyCallback(callback FilterReadyCallback) {
81121 fm.onFiltersReady = callback
@@ -269,6 +309,22 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) {
269309 fm.compileMutex.Lock()
270310 defer fm.compileMutex.Unlock()
271311
312+ // Add whitelist rules from config (ignore-previous-rules for whitelisted domains)
313+ // Update cache if whitelist changed, then use cached rules
314+ cfg := config.Get()
315+
316+ // Only apply filters if content filtering is enabled
317+ if !cfg.ContentFiltering.Enabled {
318+ logging.Info("[filtering] Content filtering is DISABLED in config - no ad blocking active")
319+ return
320+ }
321+
322+ fm.updateWhitelistCache(cfg.ContentFiltering.Whitelist)
323+ if len(fm.cachedWhitelistRules) > 0 {
324+ filters.NetworkRules = append(filters.NetworkRules, fm.cachedWhitelistRules...)
325+ logging.Info(fmt.Sprintf("[filtering] Applied %d whitelist rules (ignore-previous-rules)", len(fm.cachedWhitelistRules)))
326+ }
327+
272328 // Add uBlock Origin style scriptlets to handle broken page loading
273329
274330 // Instead of blocking completely, add exception rules for critical ad scripts that break page loading
@@ -7,9 +7,11 @@ import (
7 "encoding/json"7 "encoding/json"
8 "fmt"8 "fmt"
9 "net/http"9 "net/http"
10+ "strings"
10 "sync"11 "sync"
11 "time"12 "time"
12 13
14+ "github.com/bnema/dumber/internal/config"
13 "github.com/bnema/dumber/internal/filtering/converter"15 "github.com/bnema/dumber/internal/filtering/converter"
14 "github.com/bnema/dumber/internal/filtering/cosmetic"16 "github.com/bnema/dumber/internal/filtering/cosmetic"
15 "github.com/bnema/dumber/internal/logging"17 "github.com/bnema/dumber/internal/logging"
@@ -33,6 +35,9 @@ type FilterManager struct {
33 compileMutex sync.RWMutex35 compileMutex sync.RWMutex
34 compiled *CompiledFilters36 compiled *CompiledFilters
35 onFiltersReady FilterReadyCallback37 onFiltersReady FilterReadyCallback
38+ // Cached whitelist rules to avoid recomputing on every filter application
39+ cachedWhitelistRules []converter.WebKitRule
40+ cachedWhitelistHash string
36 }41 }
37 42
38 // FilterStore defines the interface for filter storage operations43 // FilterStore defines the interface for filter storage operations
@@ -76,6 +81,41 @@ func NewFilterManager(store FilterStore, compiler FilterCompiler) *FilterManager
76 }81 }
77 }82 }
78 83
84+// updateWhitelistCache rebuilds the cached whitelist rules from config if needed
85+func (fm *FilterManager) updateWhitelistCache(whitelist []string) {
86+ // Create a hash of the whitelist to detect changes
87+ currentHash := strings.Join(whitelist, "|")
88+
89+ // Only rebuild if whitelist changed
90+ if fm.cachedWhitelistHash == currentHash && fm.cachedWhitelistRules != nil {
91+ return
92+ }
93+
94+ // Rebuild cache with pre-allocation
95+ rules := make([]converter.WebKitRule, 0, len(whitelist))
96+ for _, domain := range whitelist {
97+ // Create a regex pattern that matches all URLs containing this domain
98+ // Escape special regex characters in the domain
99+ escapedDomain := strings.ReplaceAll(domain, ".", "\\.")
100+ // Match any URL containing this domain (with protocol and optional path)
101+ urlPattern := ".*" + escapedDomain
102+
103+ rules = append(rules, converter.WebKitRule{
104+ Trigger: converter.Trigger{
105+ URLFilter: urlPattern,
106+ },
107+ Action: converter.Action{
108+ Type: converter.ActionTypeIgnorePreviousRules,
109+ },
110+ })
111+ logging.Info(fmt.Sprintf("[filtering] Created whitelist rule: %s → pattern: %s", domain, urlPattern))
112+ }
113+
114+ fm.cachedWhitelistRules = rules
115+ fm.cachedWhitelistHash = currentHash
116+ logging.Info(fmt.Sprintf("[filtering] Whitelist cache updated with %d domains", len(whitelist)))
117+}
118+
79 // SetFiltersReadyCallback sets a callback to be called when filters are loaded/updated119 // SetFiltersReadyCallback sets a callback to be called when filters are loaded/updated
80 func (fm *FilterManager) SetFiltersReadyCallback(callback FilterReadyCallback) {120 func (fm *FilterManager) SetFiltersReadyCallback(callback FilterReadyCallback) {
81 fm.onFiltersReady = callback121 fm.onFiltersReady = callback
@@ -269,6 +309,22 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) {
269 fm.compileMutex.Lock()309 fm.compileMutex.Lock()
270 defer fm.compileMutex.Unlock()310 defer fm.compileMutex.Unlock()
271 311
312+ // Add whitelist rules from config (ignore-previous-rules for whitelisted domains)
313+ // Update cache if whitelist changed, then use cached rules
314+ cfg := config.Get()
315+
316+ // Only apply filters if content filtering is enabled
317+ if !cfg.ContentFiltering.Enabled {
318+ logging.Info("[filtering] Content filtering is DISABLED in config - no ad blocking active")
319+ return
320+ }
321+
322+ fm.updateWhitelistCache(cfg.ContentFiltering.Whitelist)
323+ if len(fm.cachedWhitelistRules) > 0 {
324+ filters.NetworkRules = append(filters.NetworkRules, fm.cachedWhitelistRules...)
325+ logging.Info(fmt.Sprintf("[filtering] Applied %d whitelist rules (ignore-previous-rules)", len(fm.cachedWhitelistRules)))
326+ }
327+
272 // Add uBlock Origin style scriptlets to handle broken page loading328 // Add uBlock Origin style scriptlets to handle broken page loading
273 329
274 // Instead of blocking completely, add exception rules for critical ad scripts that break page loading330 // Instead of blocking completely, add exception rules for critical ad scripts that break page loading

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