1package cmd
2
3import (
4	"fmt"
5	"io"
6	"io/fs"
7	"os"
8	"regexp"
9	"sort"
10	"strings"
11
12	"github.com/charmbracelet/glamour"
13	"github.com/spf13/cobra"
14	"go.uber.org/multierr"
15	"golang.org/x/term"
16
17	"github.com/twpayne/chezmoi/v2/docs"
18)
19
20type docsCmdConfig struct {
21	MaxWidth int    `mapstructure:"maxWidth"`
22	Pager    string `mapstructure:"pager"`
23}
24
25func (c *Config) newDocsCmd() *cobra.Command {
26	docsCmd := &cobra.Command{
27		Use:               "docs [regexp]",
28		Short:             "Print documentation",
29		Long:              mustLongHelp("docs"),
30		ValidArgsFunction: c.docsCmdValidArgs,
31		Example:           example("docs"),
32		Args:              cobra.MaximumNArgs(1),
33		RunE:              c.runDocsCmd,
34		Annotations: map[string]string{
35			doesNotRequireValidConfig: "true",
36		},
37	}
38
39	flags := docsCmd.Flags()
40	flags.IntVar(&c.Docs.MaxWidth, "max-width", c.Docs.MaxWidth, "Set maximum output width")
41	flags.StringVar(&c.Docs.Pager, "pager", c.Docs.Pager, "Set pager")
42
43	return docsCmd
44}
45
46// docsCmdValidArgs returns the completions for the docs command.
47func (c *Config) docsCmdValidArgs(
48	cmd *cobra.Command, args []string, toComplete string,
49) ([]string, cobra.ShellCompDirective) {
50	var completions []string
51	if err := fs.WalkDir(docs.FS, ".", func(path string, dirEntry fs.DirEntry, err error) error {
52		if err != nil {
53			return err
54		}
55		if dirEntry.IsDir() {
56			return nil
57		}
58		completion := strings.ToLower(path)
59		if strings.HasPrefix(completion, toComplete) {
60			completions = append(completions, completion)
61		}
62		return nil
63	}); err != nil {
64		cobra.CompErrorln(err.Error())
65		return nil, cobra.ShellCompDirectiveError
66	}
67	sort.Strings(completions)
68	return completions, cobra.ShellCompDirectiveNoFileComp
69}
70
71func (c *Config) runDocsCmd(cmd *cobra.Command, args []string) (err error) {
72	filename := "REFERENCE.md"
73	if len(args) > 0 {
74		pattern := args[0]
75		var re *regexp.Regexp
76		if re, err = regexp.Compile(strings.ToLower(pattern)); err != nil {
77			return
78		}
79		var dirEntries []fs.DirEntry
80		if dirEntries, err = docs.FS.ReadDir("."); err != nil {
81			return
82		}
83		var filenames []string
84		for _, dirEntry := range dirEntries {
85			var fileInfo fs.FileInfo
86			if fileInfo, err = dirEntry.Info(); err != nil {
87				return
88			}
89			if fileInfo.Mode().Type() != 0 {
90				continue
91			}
92			if filename := dirEntry.Name(); re.FindStringIndex(strings.ToLower(filename)) != nil {
93				filenames = append(filenames, filename)
94			}
95		}
96		switch {
97		case len(filenames) == 0:
98			err = fmt.Errorf("%s: no matching files", pattern)
99			return
100		case len(filenames) == 1:
101			filename = filenames[0]
102		default:
103			err = fmt.Errorf("%s: ambiguous pattern, matches %s", pattern, strings.Join(filenames, ", "))
104			return
105		}
106	}
107
108	var file fs.File
109	if file, err = docs.FS.Open(filename); err != nil {
110		return
111	}
112	defer func() {
113		err = multierr.Append(err, file.Close())
114	}()
115	var documentData []byte
116	if documentData, err = io.ReadAll(file); err != nil {
117		return
118	}
119
120	width := 80
121	if stdout, ok := c.stdout.(*os.File); ok && term.IsTerminal(int(stdout.Fd())) {
122		if width, _, err = term.GetSize(int(stdout.Fd())); err != nil {
123			return
124		}
125	}
126	if c.Docs.MaxWidth != 0 && width > c.Docs.MaxWidth {
127		width = c.Docs.MaxWidth
128	}
129
130	var termRenderer *glamour.TermRenderer
131	if termRenderer, err = glamour.NewTermRenderer(
132		glamour.WithStyles(glamour.ASCIIStyleConfig),
133		glamour.WithWordWrap(width),
134	); err != nil {
135		return
136	}
137
138	var renderedData []byte
139	if renderedData, err = termRenderer.RenderBytes(documentData); err != nil {
140		return err
141	}
142
143	err = c.pageOutputString(string(renderedData), c.Docs.Pager)
144	return
145}
146