1// Package fang provides styling for cobra commands.
2package fang
3
4import (
5 "context"
6 "fmt"
7 "io"
8 "os"
9 "os/signal"
10 "runtime/debug"
11
12 "github.com/charmbracelet/colorprofile"
13 "github.com/charmbracelet/lipgloss/v2"
14 "github.com/charmbracelet/x/term"
15 mango "github.com/muesli/mango-cobra"
16 "github.com/muesli/roff"
17 "github.com/spf13/cobra"
18)
19
20const shaLen = 7
21
22// ErrorHandler handles an error, printing them to the given [io.Writer].
23//
24// Note that this will only be used if the STDERR is a terminal, and should
25// be used for styling only.
26type ErrorHandler = func(w io.Writer, styles Styles, err error)
27
28// ColorSchemeFunc gets a [lipgloss.LightDarkFunc] and returns a [ColorScheme].
29type ColorSchemeFunc = func(lipgloss.LightDarkFunc) ColorScheme
30
31type settings struct {
32 completions bool
33 manpages bool
34 skipVersion bool
35 version string
36 commit string
37 colorscheme ColorSchemeFunc
38 errHandler ErrorHandler
39 signals []os.Signal
40}
41
42// Option changes fang settings.
43type Option func(*settings)
44
45// WithoutCompletions disables completions.
46func WithoutCompletions() Option {
47 return func(s *settings) {
48 s.completions = false
49 }
50}
51
52// WithoutManpage disables man pages.
53func WithoutManpage() Option {
54 return func(s *settings) {
55 s.manpages = false
56 }
57}
58
59// WithColorSchemeFunc sets a function that return colorscheme.
60func WithColorSchemeFunc(cs ColorSchemeFunc) Option {
61 return func(s *settings) {
62 s.colorscheme = cs
63 }
64}
65
66// WithTheme sets the colorscheme.
67//
68// Deprecated: use [WithColorSchemeFunc] instead.
69func WithTheme(theme ColorScheme) Option {
70 return func(s *settings) {
71 s.colorscheme = func(lipgloss.LightDarkFunc) ColorScheme {
72 return theme
73 }
74 }
75}
76
77// WithVersion sets the version.
78func WithVersion(version string) Option {
79 return func(s *settings) {
80 s.version = version
81 }
82}
83
84// WithoutVersion skips the `-v`/`--version` functionality.
85func WithoutVersion() Option {
86 return func(s *settings) {
87 s.skipVersion = true
88 }
89}
90
91// WithCommit sets the commit SHA.
92func WithCommit(commit string) Option {
93 return func(s *settings) {
94 s.commit = commit
95 }
96}
97
98// WithErrorHandler sets the error handler.
99func WithErrorHandler(handler ErrorHandler) Option {
100 return func(s *settings) {
101 s.errHandler = handler
102 }
103}
104
105// WithNotifySignal sets the signals that should interrupt the execution of the
106// program.
107func WithNotifySignal(signals ...os.Signal) Option {
108 return func(s *settings) {
109 s.signals = signals
110 }
111}
112
113// Execute applies fang to the command and executes it.
114func Execute(ctx context.Context, root *cobra.Command, options ...Option) error {
115 opts := settings{
116 manpages: true,
117 completions: true,
118 colorscheme: DefaultColorScheme,
119 errHandler: DefaultErrorHandler,
120 }
121
122 for _, option := range options {
123 option(&opts)
124 }
125
126 helpFunc := func(c *cobra.Command, _ []string) {
127 w := colorprofile.NewWriter(c.OutOrStdout(), os.Environ())
128 helpFn(c, w, makeStyles(mustColorscheme(opts.colorscheme)))
129 }
130
131 root.SilenceUsage = true
132 root.SilenceErrors = true
133 if !opts.skipVersion {
134 root.Version = buildVersion(opts)
135 }
136 root.SetHelpFunc(helpFunc)
137
138 if opts.manpages {
139 root.AddCommand(&cobra.Command{
140 Use: "man",
141 Short: "Generates manpages",
142 SilenceUsage: true,
143 DisableFlagsInUseLine: true,
144 Hidden: true,
145 Args: cobra.NoArgs,
146 RunE: func(cmd *cobra.Command, _ []string) error {
147 page, err := mango.NewManPage(1, cmd.Root())
148 if err != nil {
149 //nolint:wrapcheck
150 return err
151 }
152 _, err = fmt.Fprint(os.Stdout, page.Build(roff.NewDocument()))
153 //nolint:wrapcheck
154 return err
155 },
156 })
157 }
158
159 if !opts.completions {
160 root.CompletionOptions.DisableDefaultCmd = true
161 }
162
163 if len(opts.signals) > 0 {
164 var cancel context.CancelFunc
165 ctx, cancel = signal.NotifyContext(ctx, opts.signals...)
166 defer cancel()
167 }
168
169 if err := root.ExecuteContext(ctx); err != nil {
170 if w, ok := root.ErrOrStderr().(term.File); ok {
171 // if stderr is not a tty, simply print the error without any
172 // styling or going through an [ErrorHandler]:
173 if !term.IsTerminal(w.Fd()) {
174 _, _ = fmt.Fprintln(w, err.Error())
175 return err //nolint:wrapcheck
176 }
177 }
178 w := colorprofile.NewWriter(root.ErrOrStderr(), os.Environ())
179 opts.errHandler(w, makeStyles(mustColorscheme(opts.colorscheme)), err)
180 return err //nolint:wrapcheck
181 }
182 return nil
183}
184
185func buildVersion(opts settings) string {
186 commit := opts.commit
187 version := opts.version
188 if version == "" {
189 if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
190 version = info.Main.Version
191 commit = getKey(info, "vcs.revision")
192 } else {
193 version = "unknown (built from source)"
194 }
195 }
196 if len(commit) >= shaLen {
197 version += " (" + commit[:shaLen] + ")"
198 }
199 return version
200}
201
202func getKey(info *debug.BuildInfo, key string) string {
203 if info == nil {
204 return ""
205 }
206 for _, iter := range info.Settings {
207 if iter.Key == key {
208 return iter.Value
209 }
210 }
211 return ""
212}