detached 1805145

fix(app): name the config key in capture directory errors

commit
18051453d7b81e4cc79b7c29976a9b25394770d5
author
brice · · 2026-09-30 16:01 UTC
parent
a7c942d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +13 -13
M README.md +1 -1
@@ -38,7 +38,7 @@ video.dir = ~/Videos
3838 ```
3939
4040 - `selector.grid`: `on` (default) or `off` to hide the guides on startup.
41-- `screenshot.dir`, `video.dir`: an absolute path or `~/…`; relative paths are errors. The directory is created (private) when missing. Absent, the XDG defaults below apply.
41+- `screenshot.dir`, `video.dir`: an absolute path or `~/…`; relative paths are errors. Absent, the XDG defaults below apply. Before the selector opens, nefercap creates (private) and checks each directory the command can use: both for all-in-one, one for `shot` or `rec`.
4242 - `screenshot.output`: `file` (default), `file+clipboard` (save, then copy the same PNG) or `clipboard` (copy only; no file, no path printed).
4343
4444 Each key may appear once; blank lines and `#` comment lines are allowed; unknown keys, duplicates and invalid values are errors reported before the selector opens. `screenshot.output` and the directories apply to all-in-one, `shot` and `rec`; the scripted `screenshot` and `record` never read them. On `shot`, `-clipboard` alone copies only, `-clipboard -file PATH` saves and copies, and `-file PATH` alone saves only, whatever `screenshot.output` says. **G** changes visibility for the current monitor's overlay without rewriting the config. Selection dimensions are logical pixels; the captured file's dimensions depend on the monitor scaling.
@@ -38,7 +38,7 @@ video.dir = ~/Videos
38 ```38 ```
39 39
40 - `selector.grid`: `on` (default) or `off` to hide the guides on startup.40 - `selector.grid`: `on` (default) or `off` to hide the guides on startup.
41-- `screenshot.dir`, `video.dir`: an absolute path or `~/…`; relative paths are errors. The directory is created (private) when missing. Absent, the XDG defaults below apply.41+- `screenshot.dir`, `video.dir`: an absolute path or `~/…`; relative paths are errors. Absent, the XDG defaults below apply. Before the selector opens, nefercap creates (private) and checks each directory the command can use: both for all-in-one, one for `shot` or `rec`.
42 - `screenshot.output`: `file` (default), `file+clipboard` (save, then copy the same PNG) or `clipboard` (copy only; no file, no path printed).42 - `screenshot.output`: `file` (default), `file+clipboard` (save, then copy the same PNG) or `clipboard` (copy only; no file, no path printed).
43 43
44 Each key may appear once; blank lines and `#` comment lines are allowed; unknown keys, duplicates and invalid values are errors reported before the selector opens. `screenshot.output` and the directories apply to all-in-one, `shot` and `rec`; the scripted `screenshot` and `record` never read them. On `shot`, `-clipboard` alone copies only, `-clipboard -file PATH` saves and copies, and `-file PATH` alone saves only, whatever `screenshot.output` says. **G** changes visibility for the current monitor's overlay without rewriting the config. Selection dimensions are logical pixels; the captured file's dimensions depend on the monitor scaling.44 Each key may appear once; blank lines and `#` comment lines are allowed; unknown keys, duplicates and invalid values are errors reported before the selector opens. `screenshot.output` and the directories apply to all-in-one, `shot` and `rec`; the scripted `screenshot` and `record` never read them. On `shot`, `-clipboard` alone copies only, `-clipboard -file PATH` saves and copies, and `-file PATH` alone saves only, whatever `screenshot.output` says. **G** changes visibility for the current monitor's overlay without rewriting the config. Selection dimensions are logical pixels; the captured file's dimensions depend on the monitor scaling.
M internal/app/destination_test.go +10 -10
@@ -25,20 +25,20 @@ func TestCheckDirsBeforeSelector(t *testing.T) {
2525 name string
2626 options cli.Options
2727 settings config.Settings
28- wantErr bool
28+ wantKey string // config key named in the error; empty means no error
2929 }{
30- {"all-in-one bad video dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: file, ScreenshotDir: good}, true},
31- {"all-in-one bad screenshot dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: good, ScreenshotDir: file}, true},
32- {"all-in-one clipboard skips screenshot dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: good, ScreenshotDir: file, Output: config.OutputClipboard}, false},
33- {"shot ignores video dir", cli.Options{Command: cli.Shot}, config.Settings{VideoDir: file, ScreenshotDir: good}, false},
34- {"rec ignores screenshot dir", cli.Options{Command: cli.Rec}, config.Settings{VideoDir: good, ScreenshotDir: file}, false},
35- {"rec bad video dir", cli.Options{Command: cli.Rec}, config.Settings{VideoDir: file}, true},
36- {"explicit -file skips dirs", cli.Options{Command: cli.Shot, Path: "x.png"}, config.Settings{ScreenshotDir: file}, false},
30+ {"all-in-one bad video dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: file, ScreenshotDir: good}, "video.dir"},
31+ {"all-in-one bad screenshot dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: good, ScreenshotDir: file}, "screenshot.dir"},
32+ {"all-in-one clipboard skips screenshot dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: good, ScreenshotDir: file, Output: config.OutputClipboard}, ""},
33+ {"shot ignores video dir", cli.Options{Command: cli.Shot}, config.Settings{VideoDir: file, ScreenshotDir: good}, ""},
34+ {"rec ignores screenshot dir", cli.Options{Command: cli.Rec}, config.Settings{VideoDir: good, ScreenshotDir: file}, ""},
35+ {"rec bad video dir", cli.Options{Command: cli.Rec}, config.Settings{VideoDir: file}, "video.dir"},
36+ {"explicit -file skips dirs", cli.Options{Command: cli.Shot, Path: "x.png"}, config.Settings{ScreenshotDir: file}, ""},
3737 } {
3838 t.Run(tc.name, func(t *testing.T) {
3939 err := checkDirs(tc.options, tc.settings)
40- if tc.wantErr {
41- assert.Error(t, err)
40+ if tc.wantKey != "" {
41+ assert.ErrorContains(t, err, tc.wantKey+":")
4242 } else {
4343 assert.NoError(t, err)
4444 }
@@ -25,20 +25,20 @@ func TestCheckDirsBeforeSelector(t *testing.T) {
25 name string25 name string
26 options cli.Options26 options cli.Options
27 settings config.Settings27 settings config.Settings
28- wantErr bool28+ wantKey string // config key named in the error; empty means no error
29 }{29 }{
30- {"all-in-one bad video dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: file, ScreenshotDir: good}, true},30+ {"all-in-one bad video dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: file, ScreenshotDir: good}, "video.dir"},
31- {"all-in-one bad screenshot dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: good, ScreenshotDir: file}, true},31+ {"all-in-one bad screenshot dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: good, ScreenshotDir: file}, "screenshot.dir"},
32- {"all-in-one clipboard skips screenshot dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: good, ScreenshotDir: file, Output: config.OutputClipboard}, false},32+ {"all-in-one clipboard skips screenshot dir", cli.Options{Command: cli.AllInOne}, config.Settings{VideoDir: good, ScreenshotDir: file, Output: config.OutputClipboard}, ""},
33- {"shot ignores video dir", cli.Options{Command: cli.Shot}, config.Settings{VideoDir: file, ScreenshotDir: good}, false},33+ {"shot ignores video dir", cli.Options{Command: cli.Shot}, config.Settings{VideoDir: file, ScreenshotDir: good}, ""},
34- {"rec ignores screenshot dir", cli.Options{Command: cli.Rec}, config.Settings{VideoDir: good, ScreenshotDir: file}, false},34+ {"rec ignores screenshot dir", cli.Options{Command: cli.Rec}, config.Settings{VideoDir: good, ScreenshotDir: file}, ""},
35- {"rec bad video dir", cli.Options{Command: cli.Rec}, config.Settings{VideoDir: file}, true},35+ {"rec bad video dir", cli.Options{Command: cli.Rec}, config.Settings{VideoDir: file}, "video.dir"},
36- {"explicit -file skips dirs", cli.Options{Command: cli.Shot, Path: "x.png"}, config.Settings{ScreenshotDir: file}, false},36+ {"explicit -file skips dirs", cli.Options{Command: cli.Shot, Path: "x.png"}, config.Settings{ScreenshotDir: file}, ""},
37 } {37 } {
38 t.Run(tc.name, func(t *testing.T) {38 t.Run(tc.name, func(t *testing.T) {
39 err := checkDirs(tc.options, tc.settings)39 err := checkDirs(tc.options, tc.settings)
40- if tc.wantErr {40+ if tc.wantKey != "" {
41- assert.Error(t, err)41+ assert.ErrorContains(t, err, tc.wantKey+":")
42 } else {42 } else {
43 assert.NoError(t, err)43 assert.NoError(t, err)
44 }44 }
M internal/app/interactive.go +2 -2
@@ -64,12 +64,12 @@ func checkDirs(options cli.Options, settings config.Settings) error {
6464 all := options.Command == cli.AllInOne
6565 if all || options.Command == cli.Rec {
6666 if _, err := destination.VideoIn(settings.VideoDir); err != nil {
67- return err
67+ return fmt.Errorf("video.dir: %w", err)
6868 }
6969 }
7070 if (all || options.Command == cli.Shot) && !options.Clipboard && settings.Output != config.OutputClipboard {
7171 if _, err := destination.ScreenshotIn(settings.ScreenshotDir); err != nil {
72- return err
72+ return fmt.Errorf("screenshot.dir: %w", err)
7373 }
7474 }
7575 return nil
@@ -64,12 +64,12 @@ func checkDirs(options cli.Options, settings config.Settings) error {
64 all := options.Command == cli.AllInOne64 all := options.Command == cli.AllInOne
65 if all || options.Command == cli.Rec {65 if all || options.Command == cli.Rec {
66 if _, err := destination.VideoIn(settings.VideoDir); err != nil {66 if _, err := destination.VideoIn(settings.VideoDir); err != nil {
67- return err67+ return fmt.Errorf("video.dir: %w", err)
68 }68 }
69 }69 }
70 if (all || options.Command == cli.Shot) && !options.Clipboard && settings.Output != config.OutputClipboard {70 if (all || options.Command == cli.Shot) && !options.Clipboard && settings.Output != config.OutputClipboard {
71 if _, err := destination.ScreenshotIn(settings.ScreenshotDir); err != nil {71 if _, err := destination.ScreenshotIn(settings.ScreenshotDir); err != nil {
72- return err72+ return fmt.Errorf("screenshot.dir: %w", err)
73 }73 }
74 }74 }
75 return nil75 return nil

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