detached 6dc62e7

feat(cli): add a version command

commit
6dc62e72cc7d0db0d2382c6221b612afb3a6a7da
author
brice · · 2026-10-01 02:57 UTC
committer
brice · · 2026-10-01 03:03 UTC
parent
56b8a1f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +56 -0
M README.md +1 -0
@@ -76,6 +76,7 @@ nefercap shot # selector, screenshot only
7676 nefercap rec # selector, recording only
7777 nefercap stop # stop the active recording
7878 nefercap status # print the recording state
79+nefercap version # print the version
7980 ```
8081
8182 Scripted commands never open the selector:
@@ -76,6 +76,7 @@ nefercap shot # selector, screenshot only
76 nefercap rec # selector, recording only76 nefercap rec # selector, recording only
77 nefercap stop # stop the active recording77 nefercap stop # stop the active recording
78 nefercap status # print the recording state78 nefercap status # print the recording state
79+nefercap version # print the version
79 ```80 ```
80 81
81 Scripted commands never open the selector:82 Scripted commands never open the selector:
M cmd/nefercap/main.go +18 -0
@@ -5,6 +5,7 @@ import (
55 "errors"
66 "flag"
77 "fmt"
8+ "io"
89 "os"
910 "os/signal"
1011 "syscall"
@@ -14,6 +15,9 @@ import (
1415 "github.com/bnema/nefercap/internal/logging"
1516 )
1617
18+// version is set at build time: -ldflags "-X main.version=1.2.3".
19+var version = "dev"
20+
1721 func main() { os.Exit(run()) }
1822
1923 // Only the invocation's bare signal cancellation is a successful interruption.
@@ -22,7 +26,21 @@ func interrupted(ctx context.Context, err error) bool {
2226 return err == context.Canceled && ctx.Err() != nil
2327 }
2428
29+// printVersion implements `nefercap version`. It runs before flag parsing so
30+// it needs no Wayland connection or config.
31+func printVersion(args []string, stdout, stderr io.Writer) int {
32+ if len(args) > 0 {
33+ fmt.Fprintf(stderr, "unexpected argument %q\n", args[0])
34+ return 2
35+ }
36+ fmt.Fprintln(stdout, version)
37+ return 0
38+}
39+
2540 func run() int {
41+ if len(os.Args) > 1 && os.Args[1] == "version" {
42+ return printVersion(os.Args[2:], os.Stdout, os.Stderr)
43+ }
2644 options, err := cli.Parse(os.Args[1:], os.Stderr)
2745 if errors.Is(err, flag.ErrHelp) {
2846 return 0
@@ -5,6 +5,7 @@ import (
5 "errors"5 "errors"
6 "flag"6 "flag"
7 "fmt"7 "fmt"
8+ "io"
8 "os"9 "os"
9 "os/signal"10 "os/signal"
10 "syscall"11 "syscall"
@@ -14,6 +15,9 @@ import (
14 "github.com/bnema/nefercap/internal/logging"15 "github.com/bnema/nefercap/internal/logging"
15 )16 )
16 17
18+// version is set at build time: -ldflags "-X main.version=1.2.3".
19+var version = "dev"
20+
17 func main() { os.Exit(run()) }21 func main() { os.Exit(run()) }
18 22
19 // Only the invocation's bare signal cancellation is a successful interruption.23 // Only the invocation's bare signal cancellation is a successful interruption.
@@ -22,7 +26,21 @@ func interrupted(ctx context.Context, err error) bool {
22 return err == context.Canceled && ctx.Err() != nil26 return err == context.Canceled && ctx.Err() != nil
23 }27 }
24 28
29+// printVersion implements `nefercap version`. It runs before flag parsing so
30+// it needs no Wayland connection or config.
31+func printVersion(args []string, stdout, stderr io.Writer) int {
32+ if len(args) > 0 {
33+ fmt.Fprintf(stderr, "unexpected argument %q\n", args[0])
34+ return 2
35+ }
36+ fmt.Fprintln(stdout, version)
37+ return 0
38+}
39+
25 func run() int {40 func run() int {
41+ if len(os.Args) > 1 && os.Args[1] == "version" {
42+ return printVersion(os.Args[2:], os.Stdout, os.Stderr)
43+ }
26 options, err := cli.Parse(os.Args[1:], os.Stderr)44 options, err := cli.Parse(os.Args[1:], os.Stderr)
27 if errors.Is(err, flag.ErrHelp) {45 if errors.Is(err, flag.ErrHelp) {
28 return 046 return 0
M cmd/nefercap/main_test.go +34 -0
@@ -1,10 +1,12 @@
11 package main
22
33 import (
4+ "bytes"
45 "context"
56 "errors"
67 "fmt"
78 "os"
9+ "strings"
810 "testing"
911 )
1012
@@ -41,3 +43,35 @@ func TestUsageExitCodes(t *testing.T) {
4143 }
4244 }
4345 }
46+
47+func TestVersion(t *testing.T) {
48+ original := version
49+ defer func() { version = original }()
50+
51+ var out, errOut bytes.Buffer
52+ if code := printVersion(nil, &out, &errOut); code != 0 || out.String() != "dev\n" || errOut.Len() != 0 {
53+ t.Fatalf("default: exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
54+ }
55+ version = "1.2.3"
56+ out.Reset()
57+ if code := printVersion(nil, &out, &errOut); code != 0 || out.String() != "1.2.3\n" {
58+ t.Fatalf("set: exit %d, stdout %q", code, out.String())
59+ }
60+ out.Reset()
61+ if code := printVersion([]string{"extra"}, &out, &errOut); code != 2 || out.Len() != 0 || !strings.Contains(errOut.String(), `"extra"`) {
62+ t.Fatalf("extra: exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
63+ }
64+}
65+
66+func TestVersionCommandRuns(t *testing.T) {
67+ original := os.Args
68+ defer func() { os.Args = original }()
69+ os.Args = []string{"nefercap", "version"}
70+ if code := run(); code != 0 {
71+ t.Fatalf("version: exit %d", code)
72+ }
73+ os.Args = []string{"nefercap", "version", "x"}
74+ if code := run(); code != 2 {
75+ t.Fatalf("version x: exit %d, want 2", code)
76+ }
77+}
@@ -1,10 +1,12 @@
1 package main1 package main
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
5 "errors"6 "errors"
6 "fmt"7 "fmt"
7 "os"8 "os"
9+ "strings"
8 "testing"10 "testing"
9 )11 )
10 12
@@ -41,3 +43,35 @@ func TestUsageExitCodes(t *testing.T) {
41 }43 }
42 }44 }
43 }45 }
46+
47+func TestVersion(t *testing.T) {
48+ original := version
49+ defer func() { version = original }()
50+
51+ var out, errOut bytes.Buffer
52+ if code := printVersion(nil, &out, &errOut); code != 0 || out.String() != "dev\n" || errOut.Len() != 0 {
53+ t.Fatalf("default: exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
54+ }
55+ version = "1.2.3"
56+ out.Reset()
57+ if code := printVersion(nil, &out, &errOut); code != 0 || out.String() != "1.2.3\n" {
58+ t.Fatalf("set: exit %d, stdout %q", code, out.String())
59+ }
60+ out.Reset()
61+ if code := printVersion([]string{"extra"}, &out, &errOut); code != 2 || out.Len() != 0 || !strings.Contains(errOut.String(), `"extra"`) {
62+ t.Fatalf("extra: exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
63+ }
64+}
65+
66+func TestVersionCommandRuns(t *testing.T) {
67+ original := os.Args
68+ defer func() { os.Args = original }()
69+ os.Args = []string{"nefercap", "version"}
70+ if code := run(); code != 0 {
71+ t.Fatalf("version: exit %d", code)
72+ }
73+ os.Args = []string{"nefercap", "version", "x"}
74+ if code := run(); code != 2 {
75+ t.Fatalf("version x: exit %d, want 2", code)
76+ }
77+}
M internal/adapters/cli/cli.go +3 -0
@@ -92,6 +92,9 @@ Script commands (never open the selector):
9292 screenshot save a PNG screenshot
9393 record record silent video until interrupted or target video length is reached
9494
95+Other:
96+ version print the nefercap version
97+
9598 Flags:
9699 -clipboard copy PNG with wl-copy; no file unless -file is given (shot, screenshot)
97100 -debug enable debug logging (all commands)
@@ -92,6 +92,9 @@ Script commands (never open the selector):
92 screenshot save a PNG screenshot92 screenshot save a PNG screenshot
93 record record silent video until interrupted or target video length is reached93 record record silent video until interrupted or target video length is reached
94 94
95+Other:
96+ version print the nefercap version
97+
95 Flags:98 Flags:
96 -clipboard copy PNG with wl-copy; no file unless -file is given (shot, screenshot)99 -clipboard copy PNG with wl-copy; no file unless -file is given (shot, screenshot)
97 -debug enable debug logging (all commands)100 -debug enable debug logging (all commands)

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