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}