main 3d41bc5

config.go RawHistoryFolder

1// Package config reads nefercap's persistent settings.
2package config
3
4import (
5	"errors"
6	"fmt"
7	"os"
8	"slices"
9	"strings"
10
11	"github.com/bnema/kvconf"
12)
13
14const (
15	maxBytes = 4096
16	maxPath  = 4096
17)
18
19// ScreenshotOutput says where an interactive screenshot goes.
20type ScreenshotOutput string
21
22const (
23	OutputFile          ScreenshotOutput = "file"
24	OutputFileClipboard ScreenshotOutput = "file+clipboard"
25	OutputClipboard     ScreenshotOutput = "clipboard"
26)
27
28// Settings holds the persistent configuration. G toggles Grid for the current
29// selection without changing the saved setting. Freeze shows a still copy of
30// the screen while a screenshot is selected, and captures that copy. The
31// directories are absolute, or empty for the XDG default.
32type Settings struct {
33	Grid          bool
34	Freeze        bool
35	ScreenshotDir string
36	VideoDir      string
37	Output        ScreenshotOutput
38}
39
40var keys = [...]string{"selector.grid", "selector.freeze", "screenshot.dir", "screenshot.output", "video.dir"}
41
42var knownKeys = strings.Join(keys[:], ", ")
43
44// A "#" only starts a comment at the start of a line, and values are never
45// unquoted. Every problem is collected, so Load can fail on the first one in
46// line order: a line without "=", an unknown key, a duplicate or a bad value.
47var options = kvconf.Options{Comments: kvconf.CommentLine, RawValues: true, MaxBytes: maxBytes}
48
49// defaults are the settings of an absent file or key.
50var defaults = Settings{Grid: true, Freeze: true, Output: OutputFile}
51
52// file is the config file as kvconf decodes it, before it becomes Settings.
53type file struct {
54	Grid     onOff            `kv:"selector.grid"`
55	Freeze   onOff            `kv:"selector.freeze"`
56	ShotDir  dirPath          `kv:"screenshot.dir"`
57	VideoDir dirPath          `kv:"video.dir"`
58	Output   ScreenshotOutput `kv:"screenshot.output" oneof:"file|file+clipboard|clipboard"`
59}
60
61// onOff accepts only "on" and "off" (kvconf's bool also takes true/yes/1).
62type onOff bool
63
64func (o *onOff) UnmarshalText(text []byte) error {
65	switch string(text) {
66	case "on":
67		*o = true
68	case "off":
69		*o = false
70	default:
71		return kvconf.ValueError("must be on or off")
72	}
73	return nil
74}
75
76// dirPath is a kvconf.Path that refuses a bare "~" and paths over maxPath.
77type dirPath string
78
79func (d *dirPath) UnmarshalText(text []byte) error {
80	if len(text) > maxPath {
81		return kvconf.ValueError(fmt.Sprintf("must be at most %d bytes", maxPath))
82	}
83	if string(text) == "~" {
84		return kvconf.ValueError("must be an absolute path or start with ~/")
85	}
86	var p kvconf.Path
87	if err := p.UnmarshalText(text); err != nil {
88		return err
89	}
90	*d = dirPath(p)
91	return nil
92}
93
94// Load reads $XDG_CONFIG_HOME/nefercap/config (or ~/.config/nefercap/config).
95// An absent file or setting enables the full monitor grid and the frozen
96// screenshot selector. Invalid settings fail explicitly rather than silently
97// changing the requested appearance.
98func Load() (Settings, error) {
99	path, err := kvconf.DefaultPath("nefercap", "config")
100	if err != nil {
101		return defaults, nil
102	}
103	data, err := kvconf.ReadFile(path, options)
104	switch {
105	case errors.Is(err, os.ErrNotExist):
106		return defaults, nil
107	case errors.Is(err, kvconf.ErrTooLarge):
108		return Settings{}, fmt.Errorf("config %s exceeds %d bytes", path, maxBytes)
109	case errors.Is(err, kvconf.ErrNotRegular):
110		return Settings{}, fmt.Errorf("config %s: expected a regular file", path)
111	case err != nil:
112		return Settings{}, fmt.Errorf("read config: %w", err)
113	}
114	entries, syntax, err := kvconf.ParseBytes(data, options)
115	if err != nil {
116		return Settings{}, fmt.Errorf("config %s: %w", path, err)
117	}
118	f := file{Grid: onOff(defaults.Grid), Freeze: onOff(defaults.Freeze), Output: defaults.Output}
119	values, err := kvconf.Decode(entries, &f, options)
120	if err != nil {
121		return Settings{}, fmt.Errorf("config %s: %w", path, err)
122	}
123	// Fail on the first problem in line order. A line without "=" or a
124	// duplicate (from ParseBytes) wins a tie, as a repeated key is wrong
125	// whatever its value.
126	var first *kvconf.Warning
127	generic := false
128	if len(syntax) > 0 {
129		first, generic = &syntax[0], true
130	}
131	if len(values) > 0 && (first == nil || values[0].Line < first.Line) {
132		first, generic = &values[0], !slices.Contains(keys[:], values[0].Key)
133	}
134	if first != nil {
135		msg := first.Key + " " + first.Msg
136		if generic {
137			msg = "expected each of " + knownKeys + " at most once as key = value"
138		}
139		return Settings{}, fmt.Errorf("config %s line %d: %s", path, first.Line, msg)
140	}
141	return Settings{
142		Grid:          bool(f.Grid),
143		Freeze:        bool(f.Freeze),
144		ScreenshotDir: string(f.ShotDir),
145		VideoDir:      string(f.VideoDir),
146		Output:        f.Output,
147	}, nil
148}

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 2

Tags 9

↑↓ moveenter openesc close