141 lines
2.9 KiB
Go
141 lines
2.9 KiB
Go
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 strings.Contains(name, cmdName) {
|
|
found = append(found, name)
|
|
}
|
|
}
|
|
|
|
if len(found) == 0 {
|
|
return nil, errors.New("no help found for topic")
|
|
} else if len(found) == 1 {
|
|
return NoResults{}, r.showHelpTopic(found[0])
|
|
}
|
|
|
|
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
|
|
}
|