@@ -3,7 +3,10 @@ package fang
@@ -20,6 +23,7 @@ import (
21 minSpace = 10
24 minSpace = 10
22 shortPad = 2
25 shortPad = 2
25var width = sync.OnceValue(func() int {
29var width = sync.OnceValue(func() int {
@@ -45,65 +49,95 @@ func helpFn(c *cobra.Command, w *colorprofile.Writer, styles Styles) {
45 blockWidth = max(blockWidth, lipgloss.Width(ex))
49 blockWidth = max(blockWidth, lipgloss.Width(ex))
47 blockWidth = min(width()-padding, blockWidth+padding)
51 blockWidth = min(width()-padding, blockWidth+padding)
52+ blockStyle := styles.Codeblock.Base.Width(blockWidth)
49− styles.Codeblock.Base = styles.Codeblock.Base.Width(blockWidth)
54+ // if the color profile is ascii or notty, or if the block has no
55+ // background color set, remove the vertical padding.
56+ if w.Profile <= colorprofile.Ascii || reflect.DeepEqual(blockStyle.GetBackground(), lipgloss.NoColor{}) {
57+ blockStyle = blockStyle.PaddingTop(0).PaddingBottom(0)
51 _, _ = fmt.Fprintln(w, styles.Title.Render("usage"))
60 _, _ = fmt.Fprintln(w, styles.Title.Render("usage"))
52− _, _ = fmt.Fprintln(w, styles.Codeblock.Base.Render(usage))
61+ _, _ = fmt.Fprintln(w, blockStyle.Render(usage))
53 if len(examples) > 0 {
62 if len(examples) > 0 {
54− cw := styles.Codeblock.Base.GetWidth() - styles.Codeblock.Base.GetHorizontalPadding()
63+ cw := blockStyle.GetWidth() - blockStyle.GetHorizontalPadding()
55 _, _ = fmt.Fprintln(w, styles.Title.Render("examples"))
64 _, _ = fmt.Fprintln(w, styles.Title.Render("examples"))
56 for i, example := range examples {
65 for i, example := range examples {
57 if lipgloss.Width(example) > cw {
66 if lipgloss.Width(example) > cw {
58 examples[i] = ansi.Truncate(example, cw, "…")
67 examples[i] = ansi.Truncate(example, cw, "…")
61− _, _ = fmt.Fprintln(w, styles.Codeblock.Base.Render(strings.Join(examples, "\n")))
70+ _, _ = fmt.Fprintln(w, blockStyle.Render(strings.Join(examples, "\n")))
73+ groups, groupKeys := evalGroups(c)
64 cmds, cmdKeys := evalCmds(c, styles)
74 cmds, cmdKeys := evalCmds(c, styles)
65 flags, flagKeys := evalFlags(c, styles)
75 flags, flagKeys := evalFlags(c, styles)
66 space := calculateSpace(cmdKeys, flagKeys)
76 space := calculateSpace(cmdKeys, flagKeys)
70− _, _ = fmt.Fprintln(w, styles.Title.Render("commands"))
71− for _, k := range cmdKeys {
78+ for _, groupID := range groupKeys {
72− _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal(
79+ group := cmds[groupID]
73− lipgloss.Left,
80+ if len(group) == 0 {
74− lipgloss.NewStyle().PaddingLeft(leftPadding).Render(k),
81+ continue
75− strings.Repeat(" ", space-lipgloss.Width(k)),
83+ renderGroup(w, styles, space, groups[groupID], func(yield func(string, string) bool) {
84+ for _, k := range cmdKeys {
81 if len(flags) > 0 {
96 if len(flags) > 0 {
82− _, _ = fmt.Fprintln(w, styles.Title.Render("flags"))
97+ renderGroup(w, styles, space, "flags", func(yield func(string, string) bool) {
83− for _, k := range flagKeys {
98+ for _, k := range flagKeys {
84− _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal(
86− lipgloss.NewStyle().PaddingLeft(leftPadding).Render(k),
87− strings.Repeat(" ", space-lipgloss.Width(k)),
88− flags[k],
99+ if !yield(k, flags[k]) {
93 _, _ = fmt.Fprintln(w)
106 _, _ = fmt.Fprintln(w)
109+// DefaultErrorHandler is the default [ErrorHandler] implementation.
96−func writeError(w *colorprofile.Writer, styles Styles, err error) {
110+func DefaultErrorHandler(w io.Writer, styles Styles, err error) {
97 _, _ = fmt.Fprintln(w, styles.ErrorHeader.String())
111 _, _ = fmt.Fprintln(w, styles.ErrorHeader.String())
98 _, _ = fmt.Fprintln(w, styles.ErrorText.Render(err.Error()+"."))
112 _, _ = fmt.Fprintln(w, styles.ErrorText.Render(err.Error()+"."))
99 _, _ = fmt.Fprintln(w)
113 _, _ = fmt.Fprintln(w)
114+ if isUsageError(err) {
100− _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal(
115+ _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal(
101− lipgloss.Left,
116+ lipgloss.Left,
102− styles.ErrorText.UnsetWidth().Render("Try"),
117+ styles.ErrorText.UnsetWidth().Render("Try"),
103− styles.Program.Flag.Render("--help"),
118+ styles.Program.Flag.Render(" --help "),
104− styles.ErrorText.UnsetWidth().UnsetMargins().UnsetTransform().PaddingLeft(1).Render("for usage."),
119+ styles.ErrorText.UnsetWidth().UnsetMargins().UnsetTransform().Render("for usage."),
106− _, _ = fmt.Fprintln(w)
121+ _, _ = fmt.Fprintln(w)
125+// XXX: this is a hack to detect usage errors.
126+// See: https://github.com/spf13/cobra/pull/2266
127+func isUsageError(err error) bool {
129+ for _, prefix := range []string{
130+ "flag needs an argument:",
132+ "unknown shorthand flag:",
136+ if strings.HasPrefix(s, prefix) {
109func writeLongShort(w *colorprofile.Writer, styles Styles, longShort string) {
143func writeLongShort(w *colorprofile.Writer, styles Styles, longShort string) {
@@ -118,8 +152,10 @@ var otherArgsRe = regexp.MustCompile(`(\[.*\])`)
119// styleUsage stylized styleUsage line for a given command.
153// styleUsage stylized styleUsage line for a given command.
120func styleUsage(c *cobra.Command, styles Program, complete bool) string {
154func styleUsage(c *cobra.Command, styles Program, complete bool) string {
121− // XXX: maybe use c.UseLine() here?
122 u := c.Use
155 u := c.Use
123 hasArgs := strings.Contains(u, "[args]")
159 hasArgs := strings.Contains(u, "[args]")
124 hasFlags := strings.Contains(u, "[flags]") || strings.Contains(u, "[--flags]") || c.HasFlags() || c.HasPersistentFlags() || c.HasAvailableFlags()
160 hasFlags := strings.Contains(u, "[flags]") || strings.Contains(u, "[--flags]") || c.HasFlags() || c.HasPersistentFlags() || c.HasAvailableFlags()
125 hasCommands := strings.Contains(u, "[command]") || c.HasAvailableSubCommands()
161 hasCommands := strings.Contains(u, "[command]") || c.HasAvailableSubCommands()
@@ -139,34 +175,38 @@ func styleUsage(c *cobra.Command, styles Program, complete bool) string {
140 u = strings.TrimSpace(u)
176 u = strings.TrimSpace(u)
142− useLine := []string{
178+ useLine := []string{}
143− styles.Name.Render(u),
145− if !complete {
179+ if complete {
180+ parts := strings.Fields(u)
181+ useLine = append(useLine, styles.Name.Render(parts[0]))
146− useLine[0] = styles.Command.Render(u)
183+ useLine = append(useLine, styles.Command.Render(" "+strings.Join(parts[1:], " ")))
186+ useLine = append(useLine, styles.Command.Render(u))
148 if hasCommands {
188 if hasCommands {
149 useLine = append(
189 useLine = append(
151− styles.DimmedArgument.Render("[command]"),
191+ styles.DimmedArgument.Render(" [command]"),
154 if hasArgs {
194 if hasArgs {
155 useLine = append(
195 useLine = append(
157− styles.DimmedArgument.Render("[args]"),
197+ styles.DimmedArgument.Render(" [args]"),
160 for _, arg := range otherArgs {
200 for _, arg := range otherArgs {
161 useLine = append(
201 useLine = append(
163− styles.DimmedArgument.Render(arg),
203+ styles.DimmedArgument.Render(" "+arg),
166 if hasFlags {
206 if hasFlags {
167 useLine = append(
207 useLine = append(
169− styles.DimmedArgument.Render("[--flags]"),
209+ styles.DimmedArgument.Render(" [--flags]"),
172 return lipgloss.JoinHorizontal(lipgloss.Left, useLine...)
212 return lipgloss.JoinHorizontal(lipgloss.Left, useLine...)
@@ -180,19 +220,21 @@ func styleExamples(c *cobra.Command, styles Styles) []string {
181 usage := []string{}
221 usage := []string{}
182 examples := strings.Split(c.Example, "\n")
222 examples := strings.Split(c.Example, "\n")
183 for i, line := range examples {
224 for i, line := range examples {
184 line = strings.TrimSpace(line)
225 line = strings.TrimSpace(line)
185 if (i == 0 || i == len(examples)-1) && line == "" {
226 if (i == 0 || i == len(examples)-1) && line == "" {
188− s := styleExample(c, line, styles.Codeblock)
229+ s := styleExample(c, line, indent, styles.Codeblock)
189 usage = append(usage, s)
230 usage = append(usage, s)
231+ indent = len(line) > 1 && (line[len(line)-1] == '\\' || line[len(line)-1] == '|')
192 return usage
234 return usage
195−func styleExample(c *cobra.Command, line string, styles Codeblock) string {
237+func styleExample(c *cobra.Command, line string, indent bool, styles Codeblock) string {
196 if strings.HasPrefix(line, "# ") {
238 if strings.HasPrefix(line, "# ") {
197 return lipgloss.JoinHorizontal(
239 return lipgloss.JoinHorizontal(
198 lipgloss.Left,
240 lipgloss.Left,
@@ -200,66 +242,110 @@ func styleExample(c *cobra.Command, line string, styles Codeblock) string {
203− args := strings.Fields(line)
205 var isQuotedString bool
245 var isQuotedString bool
246+ var foundProgramName bool
247+ var isRedirecting bool
248+ programName := c.Root().Name()
249+ args := strings.Fields(line)
250+ var cleanArgs []string
206 for i, arg := range args {
251 for i, arg := range args {
207− if i == 0 {
252+ isQuoteStart := arg[0] == '"' || arg[0] == '\''
253+ isQuoteEnd := arg[len(arg)-1] == '"' || arg[len(arg)-1] == '\''
254+ isFlag := arg[0] == '-'
260+ args[i] = styles.Program.DimmedArgument.Render(" ")
208− args[i] = styles.Program.Name.Render(arg)
264+ args[i] = styles.Program.DimmedArgument.Render(" ")
212− quoteStart := arg[0] == '"'
267+ if isRedirecting {
213− quoteEnd := arg[len(arg)-1] == '"'
214− flagStart := arg[0] == '-'
215− if i == 1 && !quoteStart && !flagStart {
216− args[i] = styles.Program.Command.Render(arg)
268+ args[i] += styles.Program.DimmedArgument.Render(arg)
269+ isRedirecting = false
220− isQuotedString = true
273+ switch arg {
222− if isQuotedString {
275+ if i == len(args)-1 {
223− args[i] = styles.Program.QuotedString.Render(arg)
276+ args[i] += styles.Program.DimmedArgument.Render(arg)
224− if quoteEnd {
277+ continue
225− isQuotedString = false
279+ case "|", "||", "-", "&", "&&":
280+ args[i] += styles.Program.DimmedArgument.Render(arg)
284+ if isRedirect(arg) {
230− args[i] = styles.Program.Flag.Render(arg)
285+ args[i] += styles.Program.DimmedArgument.Render(arg)
286+ isRedirecting = true
233− var dashes string
289+
290+ if !foundProgramName { //nolint:nestif
292+ args[i] += styles.Program.QuotedString.Render(arg)
293+ isQuotedString = !isQuoteEnd
234− if strings.HasPrefix(arg, "-") {
296+ if left, right, ok := strings.Cut(arg, "="); ok {
235− dashes = "-"
297+ args[i] += styles.Program.Flag.Render(left + "=")
298+ if right[0] == '"' {
299+ isQuotedString = true
300+ args[i] += styles.Program.QuotedString.Render(right)
303+ args[i] += styles.Program.Argument.Render(right)
307+ if arg == programName {
308+ args[i] += styles.Program.Name.Render(arg)
309+ foundProgramName = true
237− if strings.HasPrefix(arg, "--") {
313+
238− dashes = "--"
314+ if !isQuoteStart && !isQuotedString && !isFlag {
315+ cleanArgs = append(cleanArgs, arg)
318+ if !isQuoteStart && !isFlag && isSubCommand(c, cleanArgs, arg) {
319+ args[i] += styles.Program.Command.Render(arg)
322+ isQuotedString = isQuotedString || isQuoteStart
324+ args[i] += styles.Program.QuotedString.Render(arg)
325+ isQuotedString = !isQuoteEnd
240 // handle a flag
328 // handle a flag
241− if dashes != "" {
329+ if isFlag {
242 name, value, ok := strings.Cut(arg, "=")
330 name, value, ok := strings.Cut(arg, "=")
243− name = strings.TrimPrefix(name, dashes)
244 // it is --flag=value
331 // it is --flag=value
246− args[i] = lipgloss.JoinHorizontal(
333+ args[i] += lipgloss.JoinHorizontal(
247 lipgloss.Left,
334 lipgloss.Left,
248− styles.Program.Flag.Render(dashes+name+"="),
335+ styles.Program.Flag.Render(name+"="),
249− styles.Program.Argument.UnsetPadding().Render(value),
336+ styles.Program.Argument.Render(value),
253 // it is either --bool-flag or --flag value
340 // it is either --bool-flag or --flag value
254− args[i] = lipgloss.JoinHorizontal(
341+ args[i] += lipgloss.JoinHorizontal(
255 lipgloss.Left,
342 lipgloss.Left,
256− styles.Program.Flag.Render(dashes+name),
343+ styles.Program.Flag.Render(name),
258− // if the flag is not a bool flag, next arg continues current flag
259− nextIsFlag = !isFlagBool(c, name)
262− args[i] = styles.Program.Argument.Render(arg)
348+ args[i] += styles.Program.Argument.Render(arg)
265 return lipgloss.JoinHorizontal(
351 return lipgloss.JoinHorizontal(
@@ -284,8 +370,7 @@ func evalFlags(c *cobra.Command, styles Styles) (map[string]string, []string) {
285 parts = append(
371 parts = append(
287− styles.Program.Flag.Render("-"+f.Shorthand),
373+ styles.Program.Flag.Render("-"+f.Shorthand+" --"+f.Name),
288− styles.Program.Flag.Render("--"+f.Name),
291 key := lipgloss.JoinHorizontal(lipgloss.Left, parts...)
376 key := lipgloss.JoinHorizontal(lipgloss.Left, parts...)
@@ -303,22 +388,50 @@ func evalFlags(c *cobra.Command, styles Styles) (map[string]string, []string) {
303 return flags, keys
388 return flags, keys
391+// result is map[groupID]map[styled cmd name]styled cmd help, and the keys in
392+// the order they are defined.
306−func evalCmds(c *cobra.Command, styles Styles) (map[string]string, []string) {
393+func evalCmds(c *cobra.Command, styles Styles) (map[string](map[string]string), []string) {
307 padStyle := lipgloss.NewStyle().PaddingLeft(0) //nolint:mnd
394 padStyle := lipgloss.NewStyle().PaddingLeft(0) //nolint:mnd
308 keys := []string{}
395 keys := []string{}
309− cmds := map[string]string{}
396+ cmds := map[string]map[string]string{}
310 for _, sc := range c.Commands() {
397 for _, sc := range c.Commands() {
311 if sc.Hidden {
398 if sc.Hidden {
401+ if _, ok := cmds[sc.GroupID]; !ok {
402+ cmds[sc.GroupID] = map[string]string{}
314 key := padStyle.Render(styleUsage(sc, styles.Program, false))
404 key := padStyle.Render(styleUsage(sc, styles.Program, false))
315 help := styles.FlagDescription.Render(sc.Short)
405 help := styles.FlagDescription.Render(sc.Short)
316− cmds[key] = help
406+ cmds[sc.GroupID][key] = help
317 keys = append(keys, key)
407 keys = append(keys, key)
319 return cmds, keys
409 return cmds, keys
412+func evalGroups(c *cobra.Command) (map[string]string, []string) {
413+ // make sure the default group is the first
415+ groups := map[string]string{"": "commands"}
416+ for _, g := range c.Groups() {
417+ groups[g.ID] = g.Title
418+ ids = append(ids, g.ID)
423+func renderGroup(w io.Writer, styles Styles, space int, name string, items iter.Seq2[string, string]) {
424+ _, _ = fmt.Fprintln(w, styles.Title.Render(name))
425+ for key, help := range items {
426+ _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal(
428+ lipgloss.NewStyle().PaddingLeft(longPad).Render(key),
429+ strings.Repeat(" ", space-lipgloss.Width(key)),
322func calculateSpace(k1, k2 []string) int {
435func calculateSpace(k1, k2 []string) int {
323 const spaceBetween = 2
436 const spaceBetween = 2
324 space := minSpace
437 space := minSpace
@@ -328,13 +441,18 @@ func calculateSpace(k1, k2 []string) int {
328 return space
441 return space
331−func isFlagBool(c *cobra.Command, name string) bool {
444+func isSubCommand(c *cobra.Command, args []string, word string) bool {
332− flag := c.Flags().Lookup(name)
445+ cmd, _, _ := c.Root().Traverse(args)
333− if flag == nil && len(name) == 1 {
446+ return cmd != nil && cmd.Name() == word
334− flag = c.Flags().ShorthandLookup(name)
447+}
336− if flag == nil {
449+var redirectPrefixes = []string{">", "<", "&>", "2>", "1>", ">>", "2>>"}
451+func isRedirect(s string) bool {
452+ for _, p := range redirectPrefixes {
453+ if strings.HasPrefix(s, p) {
337− return false
454+ return true
339− return flag.Value.Type() == "bool"
457+ return false