main f2d166b

cli.go RawHistoryFolder

1// Package cli parses the neferwl command line and runs the selected command.
2package cli
3
4import (
5	"context"
6	"errors"
7	"flag"
8	"fmt"
9	"io"
10	"os"
11)
12
13// Env holds what a command uses besides its arguments.
14type Env struct {
15	Stdout, Stderr io.Writer
16	// Version is the packaged build version; empty for source builds.
17	Version string
18}
19
20// Runner runs a command with its positional arguments, after flag parsing.
21type Runner func(ctx context.Context, env Env, args []string) error
22
23// Command is one neferwl command. Setup registers the command's flags on fs
24// and returns its Runner, which reads the parsed flag values.
25type Command struct {
26	Name    string // empty for the root command
27	Args    string // positional arguments synopsis, e.g. "[path]"
28	Summary string
29	Setup   func(fs *flag.FlagSet) Runner
30}
31
32// root runs the compositor; commands are the subcommands.
33var (
34	root     = Command{Args: "[flags]", Summary: "Run the compositor.", Setup: setupRun}
35	commands = []Command{validateConfigCmd, stateCmd, versionCmd}
36)
37
38// Main runs the command line args (without the program name) and returns
39// the process exit code: 0 on success, 2 on usage errors, 1 otherwise.
40func Main(args []string, version string) int {
41	env := Env{Stdout: os.Stdout, Stderr: os.Stderr, Version: version}
42	return exitCode(env.Stderr, Execute(context.Background(), env, args))
43}
44
45// Execute selects the command named by args[0], or the root command, parses
46// its flags and runs it. -h and --help print the command usage to Stdout.
47func Execute(ctx context.Context, env Env, args []string) error {
48	cmd := lookup(args)
49	if cmd.Name != "" {
50		args = args[1:]
51	}
52	fs := flag.NewFlagSet(cmd.path(), flag.ContinueOnError)
53	fs.SetOutput(io.Discard) // errors and usage are printed once, here and in exitCode
54	run := cmd.Setup(fs)
55	if err := fs.Parse(args); err != nil {
56		if errors.Is(err, flag.ErrHelp) {
57			printUsage(env.Stdout, cmd, fs)
58			return nil
59		}
60		return cmd.usageError(err)
61	}
62	err := run(ctx, env, fs.Args())
63	var usage usageError
64	if errors.As(err, &usage) {
65		return cmd.usageError(usage.error)
66	}
67	return err
68}
69
70func lookup(args []string) Command {
71	if len(args) > 0 {
72		for _, c := range commands {
73			if args[0] == c.Name {
74				return c
75			}
76		}
77	}
78	return root
79}
80
81func (c Command) path() string {
82	if c.Name == "" {
83		return "neferwl"
84	}
85	return "neferwl " + c.Name
86}
87
88func (c Command) synopsis() string {
89	if c.Args == "" {
90		return c.path()
91	}
92	return c.path() + " " + c.Args
93}
94
95// usageError marks a bad command line; it exits with code 2.
96type usageError struct{ error }
97
98// usagef returns a usage error; Execute appends the command synopsis.
99func usagef(format string, a ...any) error {
100	return usageError{fmt.Errorf(format, a...)}
101}
102
103func (c Command) usageError(err error) error {
104	return usageError{fmt.Errorf("%w\nusage: %s (see %s --help)", err, c.synopsis(), c.path())}
105}
106
107func printUsage(w io.Writer, cmd Command, fs *flag.FlagSet) {
108	fmt.Fprintf(w, "usage: %s\n\n%s\n", cmd.synopsis(), cmd.Summary)
109	if cmd.Name == "" {
110		fmt.Fprintln(w, "\nCommands:")
111		for _, c := range commands {
112			fmt.Fprintf(w, "  %-34s %s\n", c.synopsis(), c.Summary)
113		}
114	}
115	hasFlags := false
116	fs.VisitAll(func(*flag.Flag) { hasFlags = true })
117	if hasFlags {
118		fmt.Fprintln(w, "\nFlags:")
119		fs.SetOutput(w)
120		fs.PrintDefaults()
121	}
122}
123
124// exitCode prints err to w, unless the run log already has it, and maps
125// it to the process exit code. A canceled context (SIGINT, SIGTERM) is a
126// clean exit.
127func exitCode(w io.Writer, err error) int {
128	if err == nil || errors.Is(err, context.Canceled) {
129		return 0
130	}
131	if !errors.As(err, new(loggedError)) {
132		fmt.Fprintln(w, err)
133	}
134	var usage usageError
135	if errors.As(err, &usage) {
136		return 2
137	}
138	return 1
139}

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 25

Tags 10

↑↓ moveenter openesc close