v0.27.1 c78ce83

resolver.go RawHistoryFolder

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}

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