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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 76 | 76 | nefercap rec # selector, recording only | |
| 77 | 77 | nefercap stop # stop the active recording | |
| 78 | 78 | nefercap status # print the recording state | |
| 79 | + | nefercap version # print the version | |
| 79 | 80 | ``` | |
| 80 | 81 | ||
| 81 | 82 | Scripted commands never open the selector: | |
| @@ -76,6 +76,7 @@ nefercap shot # selector, screenshot only | |||||
| 76 | nefercap rec # selector, recording only | 76 | nefercap rec # selector, recording only | ||
| 77 | nefercap stop # stop the active recording | 77 | nefercap stop # stop the active recording | ||
| 78 | nefercap status # print the recording state | 78 | 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 ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "flag" | |
| 7 | 7 | "fmt" | |
| 8 | + | "io" | |
| 8 | 9 | "os" | |
| 9 | 10 | "os/signal" | |
| 10 | 11 | "syscall" | |
| @@ -14,6 +15,9 @@ import ( | |||
| 14 | 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 | 21 | func main() { os.Exit(run()) } | |
| 18 | 22 | ||
| 19 | 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 | 26 | 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 | 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 | 44 | options, err := cli.Parse(os.Args[1:], os.Stderr) | |
| 27 | 45 | if errors.Is(err, flag.ErrHelp) { | |
| 28 | 46 | 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() != nil | 26 | 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 0 | 46 | return 0 | ||
M cmd/nefercap/main_test.go +34 -0
| @@ -1,10 +1,12 @@ | |||
| 1 | 1 | package main | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 4 | 5 | "context" | |
| 5 | 6 | "errors" | |
| 6 | 7 | "fmt" | |
| 7 | 8 | "os" | |
| 9 | + | "strings" | |
| 8 | 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 | + | } | |
| @@ -1,10 +1,12 @@ | |||||
| 1 | package main | 1 | 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): | |||
| 92 | 92 | screenshot save a PNG screenshot | |
| 93 | 93 | record record silent video until interrupted or target video length is reached | |
| 94 | 94 | ||
| 95 | + | Other: | |
| 96 | + | version print the nefercap version | |
| 97 | + | ||
| 95 | 98 | Flags: | |
| 96 | 99 | -clipboard copy PNG with wl-copy; no file unless -file is given (shot, screenshot) | |
| 97 | 100 | -debug enable debug logging (all commands) | |
| @@ -92,6 +92,9 @@ Script commands (never open the selector): | |||||
| 92 | screenshot save a PNG screenshot | 92 | screenshot save a PNG screenshot | ||
| 93 | record record silent video until interrupted or target video length is reached | 93 | 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) | ||