package bonfire import ( "context" "fmt" "io" "slices" "strings" "github.com/charmbracelet/colorprofile" ) // builtinCommands describes the root's own commands, help and list. They are // registered by NewRoot, so plugins cannot use their names. func builtinCommands() []Command { return []Command{ { Name: "help", Description: "Display help for a command", Args: []Arg{{Name: "command_name", Description: "The command name"}}, }, { Name: "list", Description: "List commands", Flags: []Flag{ {Name: "all", Description: "Show hidden commands too", Bare: true}, {Name: "format", Description: "The output format (txt or json)", Default: "txt"}, }, }, } } func builtinCommand(name string) Command { for _, c := range builtinCommands() { if c.Name == name { return c } } return Command{Name: name} } // usageName is how a binary is invoked in usage lines and examples: summer // for the tool, bin/ for an application binary. func usageName(name string) string { if name == "summer" { return name } return "bin/" + name } // lister renders the command list of one root. type lister struct { name string commands []Command version Version out io.Writer console *console } // command is the list command with its run function. func (l *lister) command() Command { c := builtinCommand("list") c.Run = func(_ context.Context, in Input, _ Output) error { all, _ := in.Flag("all") format, _ := in.Flag("format") switch format { case "", "txt": return l.render(l.console.globals, all == "true") default: return Usage(fmt.Errorf("list: unknown format %q (use txt or json)", format)) } } return c } // all returns the root's commands and its builtins. func (l *lister) all() []Command { return append(append([]Command(nil), l.commands...), builtinCommands()...) } // render writes the Artisan list: the version header, usage, the global // options and the commands, top-level names first and then one block per // namespace (the text before the first colon). func (l *lister) render(g Globals, all bool) error { p := DefaultPalette() var top []Command groups := map[string][]Command{} width := 0 for _, c := range l.all() { ns, _, namespaced := strings.Cut(c.Name, ":") if namespaced { groups[ns] = append(groups[ns], c) } else { top = append(top, c) } width = max(width, len(c.Name)) } byName := func(a, b Command) int { return strings.Compare(a.Name, b.Name) } slices.SortFunc(top, byName) namespaces := make([]string, 0, len(groups)) for ns := range groups { namespaces = append(namespaces, ns) } slices.Sort(namespaces) var b strings.Builder b.WriteString(p.Header.Render(l.version.Header(l.name)) + "\n\n") b.WriteString(p.Header.Render("Usage:") + "\n") b.WriteString(" " + usageName(l.name) + " [options] [arguments]\n\n") b.WriteString(p.Header.Render("Options:") + "\n") writeGlobalOptions(&b, p) b.WriteString("\n" + p.Header.Render("Available commands:") + "\n") line := func(c Command) { pad := strings.Repeat(" ", width-len(c.Name)+2) b.WriteString(" " + p.Success.Render(c.Name) + pad + c.Description + "\n") } for _, c := range top { line(c) } for _, ns := range namespaces { b.WriteString(" " + p.Header.Render(ns) + "\n") cmds := groups[ns] slices.SortFunc(cmds, byName) for _, c := range cmds { line(c) } } return writeStyled(l.out, g, b.String()) } // writeGlobalOptions writes the global option lines, aligned. func writeGlobalOptions(b *strings.Builder, p Palette) { width := 0 for _, o := range globalOptions { width = max(width, len(o.flags)) } for _, o := range globalOptions { pad := strings.Repeat(" ", width-len(o.flags)+2) b.WriteString(" " + p.Success.Render(o.flags) + pad + o.description + "\n") } } // writeStyled writes s to w through a colorprofile writer, so palette // styles follow --ansi, --no-ansi, NO_COLOR and the terminal. func writeStyled(w io.Writer, g Globals, s string) error { cw := &colorprofile.Writer{Forward: w, Profile: DetectProfile(w, g.Color, nil)} _, err := io.WriteString(cw, s) return err }