watcher.go RawHistoryFolder

1package config
2
3import (
4	"fmt"
5
6	"github.com/bnema/dumber/internal/logging"
7	"github.com/fsnotify/fsnotify"
8)
9
10// Watch starts watching the config file for changes and reloads automatically.
11func (m *Manager) Watch() error {
12	m.mu.Lock()
13	defer m.mu.Unlock()
14
15	if m.watching {
16		return nil // Already watching
17	}
18
19	m.viper.WatchConfig()
20	m.viper.OnConfigChange(func(e fsnotify.Event) {
21		log := logging.NewFromEnv()
22		log.Debug().Str("op", e.Op.String()).Str("file", e.Name).Msg("fsnotify config change detected")
23
24		// Acquire write lock before reload (reload modifies m.config)
25		m.mu.Lock()
26
27		// Track whether notifyCallbacksLocked will handle the unlock
28		willNotify := false
29		defer func() {
30			if !willNotify {
31				m.mu.Unlock()
32			}
33		}()
34
35		// Skip reload if this was triggered by our own Save() - the in-memory
36		// config is already correct and viper may have stale cached data
37		if m.skipNextReload {
38			log.Debug().Msg("skipping reload (triggered by own Save)")
39			m.skipNextReload = false
40			// Still need to sync viper's internal state with the file we just wrote
41			if err := m.viper.ReadInConfig(); err != nil {
42				log.Warn().Err(err).Msg("failed to sync viper config after Save")
43				// Continue anyway - in-memory config is correct, just viper internal state is off
44			}
45			willNotify = true
46			m.notifyCallbacksLocked()
47			return
48		}
49
50		log.Debug().Msg("reloading config from external change")
51		if err := m.reload(); err != nil {
52			log.Warn().Err(err).Msg("failed to reload config")
53			return // defer will unlock
54		}
55		willNotify = true
56		m.notifyCallbacksLocked()
57	})
58
59	m.watching = true
60	return nil
61}
62
63// notifyCallbacksLocked copies callbacks and config, releases lock, then notifies.
64// Must be called with m.mu held for write. Releases the lock before calling callbacks.
65func (m *Manager) notifyCallbacksLocked() {
66	config := m.config
67	callbacks := make([]func(*Config), len(m.callbacks))
68	copy(callbacks, m.callbacks)
69	m.mu.Unlock()
70
71	for _, callback := range callbacks {
72		callback(config)
73	}
74}
75
76// OnConfigChange registers a callback function to be called when config changes.
77func (m *Manager) OnConfigChange(callback func(*Config)) {
78	m.mu.Lock()
79	defer m.mu.Unlock()
80
81	m.callbacks = append(m.callbacks, callback)
82}
83
84// reload reloads the configuration (internal method, must be called with lock held for write).
85func (m *Manager) reload() error {
86	if err := m.viper.ReadInConfig(); err != nil {
87		return err
88	}
89
90	m.transformLegacyConfig()
91
92	config := &Config{}
93	if err := m.viper.Unmarshal(config); err != nil {
94		return err
95	}
96
97	// Set database path if not specified
98	if config.Database.Path == "" {
99		dbPath, err := GetDatabaseFile()
100		if err != nil {
101			return fmt.Errorf("failed to get database path: %w", err)
102		}
103		config.Database.Path = dbPath
104	}
105
106	normalizeConfig(config)
107
108	// Validate all config values
109	if err := validateConfig(config); err != nil {
110		return fmt.Errorf("configuration validation failed: %w", err)
111	}
112
113	m.config = config
114	return nil
115}
116
117// Watch starts watching the global configuration for changes.
118func Watch() error {
119	if globalManager == nil {
120		return fmt.Errorf("configuration not initialized")
121	}
122	return globalManager.Watch()
123}
124
125// OnConfigChange registers a callback for global configuration changes.
126func OnConfigChange(callback func(*Config)) {
127	if globalManager == nil {
128		return
129	}
130	globalManager.OnConfigChange(callback)
131}

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