1package fang
2
3import (
4 "cmp"
5 "fmt"
6 "os"
7 "regexp"
8 "strconv"
9 "strings"
10 "sync"
11
12 "github.com/charmbracelet/colorprofile"
13 "github.com/charmbracelet/lipgloss/v2"
14 "github.com/charmbracelet/x/ansi"
15 "github.com/charmbracelet/x/term"
16 "github.com/spf13/cobra"
17 "github.com/spf13/pflag"
18)
19
20const (
21 minSpace = 10
22 shortPad = 2
23)
24
25var width = sync.OnceValue(func() int {
26 if s := os.Getenv("__FANG_TEST_WIDTH"); s != "" {
27 w, _ := strconv.Atoi(s)
28 return w
29 }
30 w, _, err := term.GetSize(os.Stdout.Fd())
31 if err != nil {
32 return 120
33 }
34 return min(w, 120)
35})
36
37func helpFn(c *cobra.Command, w *colorprofile.Writer, styles Styles) {
38 writeLongShort(w, styles, cmp.Or(c.Long, c.Short))
39 usage := styleUsage(c, styles.Codeblock.Program, true)
40 examples := styleExamples(c, styles)
41
42 padding := styles.Codeblock.Base.GetHorizontalPadding()
43 blockWidth := lipgloss.Width(usage)
44 for _, ex := range examples {
45 blockWidth = max(blockWidth, lipgloss.Width(ex))
46 }
47 blockWidth = min(width()-padding, blockWidth+padding)
48
49 styles.Codeblock.Base = styles.Codeblock.Base.Width(blockWidth)
50
51 _, _ = fmt.Fprintln(w, styles.Title.Render("usage"))
52 _, _ = fmt.Fprintln(w, styles.Codeblock.Base.Render(usage))
53 if len(examples) > 0 {
54 cw := styles.Codeblock.Base.GetWidth() - styles.Codeblock.Base.GetHorizontalPadding()
55 _, _ = fmt.Fprintln(w, styles.Title.Render("examples"))
56 for i, example := range examples {
57 if lipgloss.Width(example) > cw {
58 examples[i] = ansi.Truncate(example, cw, "…")
59 }
60 }
61 _, _ = fmt.Fprintln(w, styles.Codeblock.Base.Render(strings.Join(examples, "\n")))
62 }
63
64 cmds, cmdKeys := evalCmds(c, styles)
65 flags, flagKeys := evalFlags(c, styles)
66 space := calculateSpace(cmdKeys, flagKeys)
67
68 leftPadding := 4
69 if len(cmds) > 0 {
70 _, _ = fmt.Fprintln(w, styles.Title.Render("commands"))
71 for _, k := range cmdKeys {
72 _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal(
73 lipgloss.Left,
74 lipgloss.NewStyle().PaddingLeft(leftPadding).Render(k),
75 strings.Repeat(" ", space-lipgloss.Width(k)),
76 cmds[k],
77 ))
78 }
79 }
80
81 if len(flags) > 0 {
82 _, _ = fmt.Fprintln(w, styles.Title.Render("flags"))
83 for _, k := range flagKeys {
84 _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal(
85 lipgloss.Left,
86 lipgloss.NewStyle().PaddingLeft(leftPadding).Render(k),
87 strings.Repeat(" ", space-lipgloss.Width(k)),
88 flags[k],
89 ))
90 }
91 }
92
93 _, _ = fmt.Fprintln(w)
94}
95
96func writeError(w *colorprofile.Writer, styles Styles, err error) {
97 _, _ = fmt.Fprintln(w, styles.ErrorHeader.String())
98 _, _ = fmt.Fprintln(w, styles.ErrorText.Render(err.Error()+"."))
99 _, _ = fmt.Fprintln(w)
100 _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal(
101 lipgloss.Left,
102 styles.ErrorText.UnsetWidth().Render("Try"),
103 styles.Program.Flag.Render("--help"),
104 styles.ErrorText.UnsetWidth().UnsetMargins().UnsetTransform().PaddingLeft(1).Render("for usage."),
105 ))
106 _, _ = fmt.Fprintln(w)
107}
108
109func writeLongShort(w *colorprofile.Writer, styles Styles, longShort string) {
110 if longShort == "" {
111 return
112 }
113 _, _ = fmt.Fprintln(w)
114 _, _ = fmt.Fprintln(w, styles.Text.Width(width()).PaddingLeft(shortPad).Render(longShort))
115}
116
117var otherArgsRe = regexp.MustCompile(`(\[.*\])`)
118
119// styleUsage stylized styleUsage line for a given command.
120func styleUsage(c *cobra.Command, styles Program, complete bool) string {
121 // XXX: maybe use c.UseLine() here?
122 u := c.Use
123 hasArgs := strings.Contains(u, "[args]")
124 hasFlags := strings.Contains(u, "[flags]") || strings.Contains(u, "[--flags]") || c.HasFlags() || c.HasPersistentFlags() || c.HasAvailableFlags()
125 hasCommands := strings.Contains(u, "[command]") || c.HasAvailableSubCommands()
126 for _, k := range []string{
127 "[args]",
128 "[flags]", "[--flags]",
129 "[command]",
130 } {
131 u = strings.ReplaceAll(u, k, "")
132 }
133
134 var otherArgs []string //nolint:prealloc
135 for _, arg := range otherArgsRe.FindAllString(u, -1) {
136 u = strings.ReplaceAll(u, arg, "")
137 otherArgs = append(otherArgs, arg)
138 }
139
140 u = strings.TrimSpace(u)
141
142 useLine := []string{
143 styles.Name.Render(u),
144 }
145 if !complete {
146 useLine[0] = styles.Command.Render(u)
147 }
148 if hasCommands {
149 useLine = append(
150 useLine,
151 styles.DimmedArgument.Render("[command]"),
152 )
153 }
154 if hasArgs {
155 useLine = append(
156 useLine,
157 styles.DimmedArgument.Render("[args]"),
158 )
159 }
160 for _, arg := range otherArgs {
161 useLine = append(
162 useLine,
163 styles.DimmedArgument.Render(arg),
164 )
165 }
166 if hasFlags {
167 useLine = append(
168 useLine,
169 styles.DimmedArgument.Render("[--flags]"),
170 )
171 }
172 return lipgloss.JoinHorizontal(lipgloss.Left, useLine...)
173}
174
175// styleExamples for a given command.
176// will print both the cmd.Use and cmd.Example bits.
177func styleExamples(c *cobra.Command, styles Styles) []string {
178 if c.Example == "" {
179 return nil
180 }
181 usage := []string{}
182 examples := strings.Split(c.Example, "\n")
183 for i, line := range examples {
184 line = strings.TrimSpace(line)
185 if (i == 0 || i == len(examples)-1) && line == "" {
186 continue
187 }
188 s := styleExample(c, line, styles.Codeblock)
189 usage = append(usage, s)
190 }
191
192 return usage
193}
194
195func styleExample(c *cobra.Command, line string, styles Codeblock) string {
196 if strings.HasPrefix(line, "# ") {
197 return lipgloss.JoinHorizontal(
198 lipgloss.Left,
199 styles.Comment.Render(line),
200 )
201 }
202
203 args := strings.Fields(line)
204 var nextIsFlag bool
205 var isQuotedString bool
206 for i, arg := range args {
207 if i == 0 {
208 args[i] = styles.Program.Name.Render(arg)
209 continue
210 }
211
212 quoteStart := arg[0] == '"'
213 quoteEnd := arg[len(arg)-1] == '"'
214 flagStart := arg[0] == '-'
215 if i == 1 && !quoteStart && !flagStart {
216 args[i] = styles.Program.Command.Render(arg)
217 continue
218 }
219 if quoteStart {
220 isQuotedString = true
221 }
222 if isQuotedString {
223 args[i] = styles.Program.QuotedString.Render(arg)
224 if quoteEnd {
225 isQuotedString = false
226 }
227 continue
228 }
229 if nextIsFlag {
230 args[i] = styles.Program.Flag.Render(arg)
231 continue
232 }
233 var dashes string
234 if strings.HasPrefix(arg, "-") {
235 dashes = "-"
236 }
237 if strings.HasPrefix(arg, "--") {
238 dashes = "--"
239 }
240 // handle a flag
241 if dashes != "" {
242 name, value, ok := strings.Cut(arg, "=")
243 name = strings.TrimPrefix(name, dashes)
244 // it is --flag=value
245 if ok {
246 args[i] = lipgloss.JoinHorizontal(
247 lipgloss.Left,
248 styles.Program.Flag.Render(dashes+name+"="),
249 styles.Program.Argument.UnsetPadding().Render(value),
250 )
251 continue
252 }
253 // it is either --bool-flag or --flag value
254 args[i] = lipgloss.JoinHorizontal(
255 lipgloss.Left,
256 styles.Program.Flag.Render(dashes+name),
257 )
258 // if the flag is not a bool flag, next arg continues current flag
259 nextIsFlag = !isFlagBool(c, name)
260 continue
261 }
262 args[i] = styles.Program.Argument.Render(arg)
263 }
264
265 return lipgloss.JoinHorizontal(
266 lipgloss.Left,
267 args...,
268 )
269}
270
271func evalFlags(c *cobra.Command, styles Styles) (map[string]string, []string) {
272 flags := map[string]string{}
273 keys := []string{}
274 c.Flags().VisitAll(func(f *pflag.Flag) {
275 if f.Hidden {
276 return
277 }
278 var parts []string
279 if f.Shorthand == "" {
280 parts = append(
281 parts,
282 styles.Program.Flag.Render("--"+f.Name),
283 )
284 } else {
285 parts = append(
286 parts,
287 styles.Program.Flag.Render("-"+f.Shorthand),
288 styles.Program.Flag.Render("--"+f.Name),
289 )
290 }
291 key := lipgloss.JoinHorizontal(lipgloss.Left, parts...)
292 help := styles.FlagDescription.Render(f.Usage)
293 if f.DefValue != "" && f.DefValue != "false" && f.DefValue != "0" && f.DefValue != "[]" {
294 help = lipgloss.JoinHorizontal(
295 lipgloss.Left,
296 help,
297 styles.FlagDefault.Render("("+f.DefValue+")"),
298 )
299 }
300 flags[key] = help
301 keys = append(keys, key)
302 })
303 return flags, keys
304}
305
306func evalCmds(c *cobra.Command, styles Styles) (map[string]string, []string) {
307 padStyle := lipgloss.NewStyle().PaddingLeft(0) //nolint:mnd
308 keys := []string{}
309 cmds := map[string]string{}
310 for _, sc := range c.Commands() {
311 if sc.Hidden {
312 continue
313 }
314 key := padStyle.Render(styleUsage(sc, styles.Program, false))
315 help := styles.FlagDescription.Render(sc.Short)
316 cmds[key] = help
317 keys = append(keys, key)
318 }
319 return cmds, keys
320}
321
322func calculateSpace(k1, k2 []string) int {
323 const spaceBetween = 2
324 space := minSpace
325 for _, k := range append(k1, k2...) {
326 space = max(space, lipgloss.Width(k)+spaceBetween)
327 }
328 return space
329}
330
331func isFlagBool(c *cobra.Command, name string) bool {
332 flag := c.Flags().Lookup(name)
333 if flag == nil && len(name) == 1 {
334 flag = c.Flags().ShorthandLookup(name)
335 }
336 if flag == nil {
337 return false
338 }
339 return flag.Value.Type() == "bool"
340}