1package colorscheme
2
3import (
4 "cmp"
5 "slices"
6 "strings"
7 "sync"
8
9 "github.com/bnema/dumber/internal/application/port"
10)
11
12const (
13 // sourceFallback indicates no detector provided the preference.
14 sourceFallback = "fallback"
15 // sourceConfig indicates the preference came from user config.
16 sourceConfig = "config"
17)
18
19// ConfigProvider provides access to the color scheme configuration.
20type ConfigProvider interface {
21 // GetColorScheme returns the configured color scheme preference.
22 // Expected values: "default", "prefer-dark", "prefer-light", "dark", "light"
23 GetColorScheme() string
24}
25
26// callbackWrapper wraps a callback function to enable pointer comparison for removal.
27type callbackWrapper struct {
28 fn func(port.ColorSchemePreference)
29}
30
31// Resolver implements port.ColorSchemeResolver.
32// It manages multiple detectors and respects config overrides.
33type Resolver struct {
34 mu sync.RWMutex
35 config ConfigProvider
36 detectors []port.ColorSchemeDetector
37 sortedDetectors []port.ColorSchemeDetector // cached sorted detectors
38 current port.ColorSchemePreference
39 callbacks []*callbackWrapper
40}
41
42// NewResolver creates a new color scheme resolver.
43// The config provider is used to check for explicit user preferences.
44func NewResolver(config ConfigProvider) *Resolver {
45 return &Resolver{
46 config: config,
47 detectors: make([]port.ColorSchemeDetector, 0),
48 current: port.ColorSchemePreference{
49 PrefersDark: true, // Default to dark until first Resolve()
50 Source: sourceFallback,
51 },
52 }
53}
54
55// Resolve implements port.ColorSchemeResolver.
56func (r *Resolver) Resolve() port.ColorSchemePreference {
57 r.mu.RLock()
58 defer r.mu.RUnlock()
59 return r.resolveInternal()
60}
61
62// resolveInternal performs the actual resolution without locking.
63// Caller must hold at least a read lock.
64func (r *Resolver) resolveInternal() port.ColorSchemePreference {
65 // Check config for explicit override first
66 if r.config != nil {
67 scheme := r.config.GetColorScheme()
68 switch strings.ToLower(scheme) {
69 case "prefer-dark", "dark":
70 return port.ColorSchemePreference{
71 PrefersDark: true,
72 Source: sourceConfig,
73 }
74 case "prefer-light", "light":
75 return port.ColorSchemePreference{
76 PrefersDark: false,
77 Source: sourceConfig,
78 }
79 // "default" or empty falls through to detector chain
80 }
81 }
82
83 // Try each detector in priority order (already sorted)
84 for _, detector := range r.sortedDetectors {
85 if !detector.Available() {
86 continue
87 }
88 if prefersDark, ok := detector.Detect(); ok {
89 return port.ColorSchemePreference{
90 PrefersDark: prefersDark,
91 Source: detector.Name(),
92 }
93 }
94 }
95
96 // Fallback to dark mode if all detectors fail
97 return port.ColorSchemePreference{
98 PrefersDark: true,
99 Source: sourceFallback,
100 }
101}
102
103// RegisterDetector implements port.ColorSchemeResolver.
104func (r *Resolver) RegisterDetector(detector port.ColorSchemeDetector) {
105 r.mu.Lock()
106 defer r.mu.Unlock()
107 r.detectors = append(r.detectors, detector)
108 r.rebuildSortedDetectors()
109}
110
111// rebuildSortedDetectors creates a sorted copy of detectors by priority (highest first).
112// Caller must hold the write lock.
113func (r *Resolver) rebuildSortedDetectors() {
114 r.sortedDetectors = make([]port.ColorSchemeDetector, len(r.detectors))
115 copy(r.sortedDetectors, r.detectors)
116 slices.SortFunc(r.sortedDetectors, func(a, b port.ColorSchemeDetector) int {
117 return cmp.Compare(b.Priority(), a.Priority()) // descending order
118 })
119}
120
121// Refresh implements port.ColorSchemeResolver.
122func (r *Resolver) Refresh() port.ColorSchemePreference {
123 r.mu.Lock()
124
125 newPref := r.resolveInternal()
126
127 // Prepare callbacks to invoke if preference changed
128 var callbacks []*callbackWrapper
129 if newPref.PrefersDark != r.current.PrefersDark {
130 r.current = newPref
131 callbacks = make([]*callbackWrapper, len(r.callbacks))
132 copy(callbacks, r.callbacks)
133 } else {
134 r.current = newPref
135 }
136
137 // Release lock before invoking callbacks to avoid deadlocks
138 r.mu.Unlock()
139
140 // Invoke callbacks outside of lock
141 for _, cb := range callbacks {
142 cb.fn(newPref)
143 }
144
145 return newPref
146}
147
148// OnChange implements port.ColorSchemeResolver.
149func (r *Resolver) OnChange(callback func(port.ColorSchemePreference)) func() {
150 r.mu.Lock()
151 defer r.mu.Unlock()
152
153 // Wrap callback to enable pointer comparison for removal
154 wrapper := &callbackWrapper{fn: callback}
155 r.callbacks = append(r.callbacks, wrapper)
156
157 // Return unregister function
158 return func() {
159 r.mu.Lock()
160 defer r.mu.Unlock()
161
162 // Find and remove callback by pointer equality
163 for i, cb := range r.callbacks {
164 if cb == wrapper {
165 r.callbacks = append(r.callbacks[:i], r.callbacks[i+1:]...)
166 return
167 }
168 }
169 }
170}