v0.13.0 976176a

injector.go RawHistoryFolder

1package cosmetic
2
3import (
4	_ "embed"
5	"encoding/json"
6	"fmt"
7	"strings"
8
9	"github.com/bnema/dumber/internal/logging"
10)
11
12//go:embed injector.js
13var cosmeticScript string
14
15// CosmeticInjector manages cosmetic filter injection
16type CosmeticInjector struct {
17	domainRules   map[string][]string
18	genericRules  []string
19	scriptEnabled bool
20}
21
22// CosmeticRule represents a cosmetic filter rule for JavaScript injection
23type CosmeticRule struct {
24	Domain   string `json:"domain,omitempty"`
25	Selector string `json:"selector"`
26}
27
28// NewCosmeticInjector creates a new cosmetic filter injector
29func NewCosmeticInjector() *CosmeticInjector {
30	return &CosmeticInjector{
31		domainRules:   make(map[string][]string),
32		genericRules:  make([]string, 0),
33		scriptEnabled: true,
34	}
35}
36
37// InjectRules stores cosmetic rules for domain-specific filtering
38func (ci *CosmeticInjector) InjectRules(rules map[string][]string) error {
39	if !ci.scriptEnabled {
40		return fmt.Errorf("cosmetic filtering disabled")
41	}
42
43	// Clear existing rules
44	ci.domainRules = make(map[string][]string)
45	ci.genericRules = make([]string, 0)
46
47	// Process rules
48	for domain, selectors := range rules {
49		if domain == "" {
50			// Generic rules (apply to all domains)
51			ci.genericRules = append(ci.genericRules, selectors...)
52		} else {
53			// Domain-specific rules
54			ci.domainRules[domain] = selectors
55		}
56	}
57
58	logging.Debug(fmt.Sprintf("Injected %d domain-specific rules and %d generic rules",
59		len(ci.domainRules), len(ci.genericRules)))
60
61	return nil
62}
63
64// GetScriptForDomain returns the complete cosmetic filtering script for a domain
65func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
66	if !ci.scriptEnabled {
67		return ""
68	}
69
70	// Build rules for this specific domain
71	var rules []CosmeticRule
72
73	// Add generic rules
74	for _, selector := range ci.genericRules {
75		rules = append(rules, CosmeticRule{
76			Selector: selector,
77		})
78	}
79
80	// Add domain-specific rules
81	if domainSelectors, exists := ci.domainRules[domain]; exists {
82		for _, selector := range domainSelectors {
83			rules = append(rules, CosmeticRule{
84				Domain:   domain,
85				Selector: selector,
86			})
87		}
88	}
89
90	// Convert rules to JSON
91	rulesJSON, err := json.Marshal(rules)
92	if err != nil {
93		logging.Error(fmt.Sprintf("Failed to marshal cosmetic rules: %v", err))
94		return cosmeticScript
95	}
96
97	// Build the complete script with domain-specific rules
98	initScript := fmt.Sprintf(`
99// Initialize cosmetic filtering for domain: %s
100(function() {
101    if (typeof window.__dumber_cosmetic_init === 'function') {
102        window.__dumber_cosmetic_init(%s);
103        if (window.self === window.top) {
104            console.log('[dumber] Cosmetic filtering initialized with %d rules');
105        }
106    } else {
107        if (window.self === window.top) {
108            console.warn('[dumber] Cosmetic filtering not available');
109        }
110    }
111})();`, domain, string(rulesJSON), len(rules))
112
113	return cosmeticScript + "\n" + initScript
114}
115
116// UpdateRulesForDomain adds new rules for a specific domain
117func (ci *CosmeticInjector) UpdateRulesForDomain(domain string, newSelectors []string) {
118	if !ci.scriptEnabled {
119		return
120	}
121
122	if len(newSelectors) == 0 {
123		return
124	}
125
126	if domain == "" {
127		// Add to generic rules
128		ci.genericRules = append(ci.genericRules, newSelectors...)
129	} else {
130		// Add to domain-specific rules
131		if _, exists := ci.domainRules[domain]; !exists {
132			ci.domainRules[domain] = make([]string, 0)
133		}
134		ci.domainRules[domain] = append(ci.domainRules[domain], newSelectors...)
135	}
136
137	logging.Debug(fmt.Sprintf("Added %d new cosmetic rules for domain: %s", len(newSelectors), domain))
138}
139
140// GetUpdateScript returns JavaScript to update rules dynamically
141func (ci *CosmeticInjector) GetUpdateScript(newSelectors []string) string {
142	if !ci.scriptEnabled || len(newSelectors) == 0 {
143		return ""
144	}
145
146	selectorsJSON, err := json.Marshal(newSelectors)
147	if err != nil {
148		logging.Error(fmt.Sprintf("Failed to marshal update selectors: %v", err))
149		return ""
150	}
151
152	return fmt.Sprintf(`
153(function() {
154    if (typeof window.__dumber_cosmetic_update === 'function') {
155        window.__dumber_cosmetic_update(%s);
156        if (window.self === window.top) {
157            console.log('[dumber] Updated cosmetic rules with %d new selectors');
158        }
159    }
160})();`, string(selectorsJSON), len(newSelectors))
161}
162
163// GetCleanupScript returns JavaScript to cleanup cosmetic filtering on navigation
164func (ci *CosmeticInjector) GetCleanupScript() string {
165	return `
166(function() {
167    if (typeof window.__dumber_cosmetic_cleanup === 'function') {
168        window.__dumber_cosmetic_cleanup();
169        if (window.self === window.top) {
170            console.log('[dumber] Cosmetic filtering cleaned up');
171        }
172    }
173})();`
174}
175
176// GetRulesForDomain returns all cosmetic rules that apply to a domain
177func (ci *CosmeticInjector) GetRulesForDomain(domain string) []string {
178	var rules []string
179
180	// Add generic rules
181	rules = append(rules, ci.genericRules...)
182
183	// Add domain-specific rules
184	if domainSelectors, exists := ci.domainRules[domain]; exists {
185		rules = append(rules, domainSelectors...)
186	}
187
188	// Also check for subdomain matches
189	domainParts := strings.Split(domain, ".")
190	if len(domainParts) > 2 {
191		// Try parent domain (e.g., "example.com" for "www.example.com")
192		parentDomain := strings.Join(domainParts[1:], ".")
193		if parentSelectors, exists := ci.domainRules[parentDomain]; exists {
194			rules = append(rules, parentSelectors...)
195		}
196	}
197
198	return rules
199}
200
201// Enable enables or disables cosmetic filtering
202func (ci *CosmeticInjector) Enable(enabled bool) {
203	ci.scriptEnabled = enabled
204	if enabled {
205		logging.Info("Cosmetic filtering enabled")
206	} else {
207		logging.Info("Cosmetic filtering disabled")
208	}
209}
210
211// IsEnabled returns whether cosmetic filtering is enabled
212func (ci *CosmeticInjector) IsEnabled() bool {
213	return ci.scriptEnabled
214}
215
216// GetStats returns statistics about loaded cosmetic rules
217func (ci *CosmeticInjector) GetStats() map[string]interface{} {
218	domainCount := len(ci.domainRules)
219	totalRules := len(ci.genericRules)
220
221	for _, selectors := range ci.domainRules {
222		totalRules += len(selectors)
223	}
224
225	return map[string]interface{}{
226		"enabled":       ci.scriptEnabled,
227		"generic_rules": len(ci.genericRules),
228		"domain_count":  domainCount,
229		"total_rules":   totalRules,
230	}
231}

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