package repl import ( "context" "errors" "fmt" "github.com/lmika/gopkgs/fp/maps" "slices" "sort" "strings" "text/tabwriter" "ucl.lmika.dev/ucl" "unicode" ) type ArgDoc struct { Name string Brief string } type Doc struct { Brief string Usage string Args []ArgDoc Detailed string } func (d Doc) config(cmdName string, r *REPL) { r.commandDocs[cmdName] = d } func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) { if args.NArgs() == 0 { return NoResults{}, r.listHelpTopics(maps.Keys(r.commandDocs)) } var cmdName string if err := args.Bind(&cmdName); err != nil { return nil, err } found := make([]string, 0) for name := range r.commandDocs { if name == cmdName { return NoResults{}, r.showHelpTopic(name) } if strings.Contains(name, cmdName) { found = append(found, name) } } if len(found) == 0 { return nil, errors.New("no help found for topic") } return NoResults{}, r.listHelpTopics(found) } func (r *REPL) listHelpTopics(topics []string) error { sort.Strings(topics) tabWriter := tabwriter.NewWriter(r.inst.Out(), 0, 0, 1, ' ', 0) for _, name := range topics { cdoc := r.commandDocs[name] if cdoc.Brief != "" { fmt.Fprintf(tabWriter, "%v\t %v\n", name, r.commandDocs[name].Brief) } else { fmt.Fprintf(tabWriter, "%v\n", name) } } tabWriter.Flush() return nil } func (r *REPL) showHelpTopic(topic string) error { docs, ok := r.commandDocs[topic] if !ok { return errors.New("no help docs found for command") } fmt.Fprintf(r.inst.Out(), "%v\n", topic) fmt.Fprintf(r.inst.Out(), " %v\n", docs.Brief) if docs.Usage != "" { fmt.Fprintln(r.inst.Out(), "\nUsage:") fmt.Fprintf(r.inst.Out(), " %v %v\n", topic, docs.Usage) } if len(docs.Args) > 0 { fmt.Fprintln(r.inst.Out(), "\nArguments:") docArgs := slices.Clone(docs.Args) sort.Slice(docArgs, func(i, j int) bool { return strings.TrimPrefix(docs.Args[i].Name, "-") < strings.TrimPrefix(docs.Args[j].Name, "-") }) tw := tabwriter.NewWriter(r.inst.Out(), 0, 0, 1, ' ', 0) for _, arg := range docArgs { fmt.Fprintf(tw, " %v\t %v\n", arg.Name, arg.Brief) } tw.Flush() } if docs.Detailed != "" { fmt.Fprintln(r.inst.Out(), "\nDetails:") lines := strings.Split(docs.Detailed, "\n") trimLeft := 0 if len(lines) == 0 { return nil } else if len(strings.TrimSpace(lines[0])) == 0 && len(lines) > 1 { // indicates that the next line should indicate the indentation trimLeft = len(lines[1]) - len(strings.TrimLeftFunc(lines[1], unicode.IsSpace)) lines = lines[1:] } for _, line := range lines { fmt.Fprintf(r.inst.Out(), " %v\n", trimSpaceLeftUpto(line, trimLeft)) } } return nil } func trimSpaceLeftUpto(s string, n int) string { if n == 0 { return s } for i, c := range s { if i >= n { return s[i:] } else if !unicode.IsSpace(c) { return s[i:] } } return s }