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