detached 1e7884d

live.go RawHistoryFolder

1package kvconf
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"io/fs"
8	"slices"
9	"sync/atomic"
10)
11
12// Load reads the config file at path into a new T: it starts from the
13// defaults (SetDefaults when *T implements Defaulter), then applies the file.
14// A missing file gives the defaults, unless opts.RequireFile is set.
15func Load[T any](path string, opts Options) (*T, []Warning, error) {
16	cfg, _, warnings, err := load[T](path, opts)
17	return cfg, warnings, err
18}
19
20// load returns nil entries, not an empty slice, when the file is missing.
21func load[T any](path string, opts Options) (*T, []Entry, []Warning, error) {
22	data, err := ReadFile(path, opts)
23	if err != nil {
24		if isNotExist(err) && !opts.RequireFile {
25			// No entries: the defaults, then Validate.
26			cfg := newDefault[T]()
27			if _, err := Decode(nil, cfg, opts); err != nil {
28				return nil, nil, nil, err
29			}
30			return cfg, nil, nil, nil
31		}
32		return nil, nil, nil, err
33	}
34	cfg, entries, warnings, err := decodeFile[T](path, data, opts)
35	if err == nil && entries == nil {
36		entries = []Entry{}
37	}
38	return cfg, entries, warnings, err
39}
40
41func decodeFile[T any](path string, data []byte, opts Options) (*T, []Entry, []Warning, error) {
42	cfg := newDefault[T]()
43	entries, warnings, err := ParseBytes(data, opts)
44	if err != nil {
45		return nil, nil, warnings, fmt.Errorf("%s: %w", path, err)
46	}
47	more, err := Decode(entries, cfg, opts)
48	warnings = append(warnings, more...)
49	if err != nil {
50		return nil, nil, warnings, fmt.Errorf("%s: %w", path, err)
51	}
52	return cfg, entries, warnings, nil
53}
54
55func newDefault[T any]() *T {
56	cfg := new(T)
57	if d, ok := any(cfg).(Defaulter); ok {
58		d.SetDefaults()
59	}
60	return cfg
61}
62
63// Change describes a reload.
64type Change[T any] struct {
65	Old, New *T
66	// Keys lists, sorted, the keys added, removed or changed.
67	Keys     []string
68	Warnings []Warning
69}
70
71// LiveOptions controls a Live config.
72type LiveOptions[T any] struct {
73	Options
74	Watch WatchOptions
75	// OnChange is called from Run after each reload that changed a value.
76	OnChange func(Change[T])
77	// OnError is called from Run when a reload fails; the current config
78	// stays in place. A removed file reports an error wrapping
79	// fs.ErrNotExist.
80	OnError func(error)
81}
82
83// Live is a config that reloads itself when its file changes. Get is safe
84// for concurrent use; a config it returned is never modified, a reload
85// replaces it.
86type Live[T any] struct {
87	path    string
88	opts    LiveOptions[T]
89	cur     atomic.Pointer[T]
90	entries []Entry // owned by Run
91	exists  bool    // whether the file was there at the last load; owned by Run
92	running atomic.Bool
93}
94
95// Open loads the config file at path and returns it with the warnings of
96// the first load. Call Run to follow its changes.
97func Open[T any](path string, opts LiveOptions[T]) (*Live[T], []Warning, error) {
98	cfg, entries, warnings, err := load[T](path, opts.Options)
99	if err != nil {
100		return nil, warnings, err
101	}
102	l := &Live[T]{path: path, opts: opts, entries: entries, exists: entries != nil}
103	l.cur.Store(cfg)
104	return l, warnings, nil
105}
106
107// Get returns the current config. Do not modify it.
108func (l *Live[T]) Get() *T { return l.cur.Load() }
109
110// Path returns the path of the config file.
111func (l *Live[T]) Path() string { return l.path }
112
113// Run watches the file and reloads it until ctx ends. A reload that fails,
114// or a removed file, keeps the current config. A save that changes no value
115// (a comment, spacing, the same file written again) reloads nothing. Run
116// returns nil when ctx ends, or the error of the watcher. Call it once.
117func (l *Live[T]) Run(ctx context.Context) error {
118	if !l.running.CompareAndSwap(false, true) {
119		return errors.New("kvconf: Live.Run called twice")
120	}
121	changed := make(chan struct{}, 1)
122	ready := make(chan struct{})
123	wopts := l.opts.Watch
124	userReady := wopts.Ready
125	wopts.Ready = ready
126	errc := make(chan error, 1)
127	go func() { errc <- Watch(ctx, l.path, wopts, changed) }()
128	select {
129	case <-ready:
130		// A save between Open and the watch would go unseen: check once.
131		l.reload()
132		if userReady != nil {
133			close(userReady)
134		}
135	case err := <-errc:
136		return err
137	}
138	for {
139		select {
140		case <-changed:
141			l.reload()
142		case err := <-errc:
143			return err
144		}
145	}
146}
147
148func (l *Live[T]) reload() {
149	data, err := ReadFile(l.path, l.opts.Options)
150	if isNotExist(err) {
151		// Keep the current config: a removed file is often about to be
152		// replaced, and an empty config is rarely what the user meant.
153		if l.exists {
154			l.exists = false
155			l.report(fmt.Errorf("%s: %w", l.path, fs.ErrNotExist))
156		}
157		return
158	}
159	if err != nil {
160		l.report(err)
161		return
162	}
163	cfg, entries, warnings, err := decodeFile[T](l.path, data, l.opts.Options)
164	if err != nil {
165		l.report(err)
166		return
167	}
168	l.exists = true
169	// Compare every assignment, not the last one: a repeated key whose
170	// last value is invalid decodes to an earlier one.
171	keys := changedKeys(l.entries, entries)
172	l.entries = entries
173	if len(keys) == 0 {
174		return
175	}
176	old := l.cur.Swap(cfg)
177	if l.opts.OnChange != nil {
178		l.opts.OnChange(Change[T]{Old: old, New: cfg, Keys: keys, Warnings: warnings})
179	}
180}
181
182// changedKeys lists, sorted, the keys whose assignments differ.
183func changedKeys(old, cur []Entry) []string {
184	group := func(es []Entry) map[string][]string {
185		m := make(map[string][]string, len(es))
186		for _, e := range es {
187			m[e.Key] = append(m[e.Key], e.Value)
188		}
189		return m
190	}
191	a, b := group(old), group(cur)
192	var keys []string
193	for k, v := range b {
194		if !slices.Equal(a[k], v) {
195			keys = append(keys, k)
196		}
197	}
198	for k := range a {
199		if _, ok := b[k]; !ok {
200			keys = append(keys, k)
201		}
202	}
203	slices.Sort(keys)
204	return keys
205}
206
207func (l *Live[T]) report(err error) {
208	if l.opts.OnError != nil {
209		l.opts.OnError(err)
210	}
211}

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 1

Tags 3

↑↓ moveenter openesc close