Merge remote-tracking branch 'origin/main'
All checks were successful
Build / build (push) Successful in 1m59s

This commit is contained in:
Leon Mika 2025-01-13 21:38:46 +11:00
commit a30c012bcd
5 changed files with 121 additions and 106 deletions

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="ucl.lmika.dev git https://lmika.dev/lmika/ucl">
</head>
<body>
</body>
</html>

View file

@ -8,6 +8,7 @@ import (
"github.com/alecthomas/participle/v2" "github.com/alecthomas/participle/v2"
"strings" "strings"
"syscall/js" "syscall/js"
"ucl.lmika.dev/repl"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
) )
@ -20,7 +21,7 @@ func invokeUCLCallback(name string, args ...any) {
func initJS(ctx context.Context) { func initJS(ctx context.Context) {
uclObj := make(map[string]any) uclObj := make(map[string]any)
inst := ucl.New(ucl.WithOut(ucl.LineHandler(func(line string) { replInst := repl.New(ucl.WithOut(ucl.LineHandler(func(line string) {
invokeUCLCallback("onOutLine", line) invokeUCLCallback("onOutLine", line)
}))) })))
@ -36,7 +37,7 @@ func initJS(ctx context.Context) {
} }
wantContinue := args[1].Bool() wantContinue := args[1].Bool()
if err := ucl.EvalAndDisplay(ctx, inst, cmdLine); err != nil { if err := replInst.EvalAndDisplay(ctx, cmdLine); err != nil {
var p participle.Error var p participle.Error
if errors.As(err, &p) && wantContinue { if errors.As(err, &p) && wantContinue {
invokeUCLCallback("onContinue") invokeUCLCallback("onContinue")
@ -50,21 +51,3 @@ func initJS(ctx context.Context) {
}) })
js.Global().Set("ucl", uclObj) js.Global().Set("ucl", uclObj)
} }
//
//type uclOut struct {
// lineBuffer *bytes.Buffer
// writeLine func(line string)
//}
//
//func (uo *uclOut) Write(p []byte) (n int, err error) {
// for _, b := range p {
// if b == '\n' {
// uo.writeLine(uo.lineBuffer.String())
// uo.lineBuffer.Reset()
// } else {
// uo.lineBuffer.WriteByte(b)
// }
// }
// return len(p), nil
//}

View file

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/lmika/gopkgs/fp/maps" "github.com/lmika/gopkgs/fp/maps"
"os"
"slices" "slices"
"sort" "sort"
"strings" "strings"
@ -31,77 +30,100 @@ func (d Doc) config(cmdName string, r *REPL) {
} }
func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) { func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) {
switch { if args.NArgs() == 0 {
case args.NArgs() == 0: return NoResults{}, r.listHelpTopics(maps.Keys(r.commandDocs))
names := maps.Keys(r.commandDocs) }
sort.Strings(names)
tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) var cmdName string
if err := args.Bind(&cmdName); err != nil {
return nil, err
}
for _, name := range names { found := make([]string, 0)
cdoc := r.commandDocs[name] for name := range r.commandDocs {
if cdoc.Brief != "" { if name == cmdName {
fmt.Fprintf(tabWriter, "%v\t %v\n", name, r.commandDocs[name].Brief) return NoResults{}, r.showHelpTopic(name)
} else {
fmt.Fprintf(tabWriter, "%v\n", name)
}
} }
tabWriter.Flush() if strings.Contains(name, cmdName) {
default: found = append(found, name)
var cmdName string
if err := args.Bind(&cmdName); err != nil {
return nil, err
}
docs, ok := r.commandDocs[cmdName]
if !ok {
return nil, errors.New("no help docs found for command")
}
fmt.Printf("%v\n", cmdName)
fmt.Printf(" %v\n", docs.Brief)
if docs.Usage != "" {
fmt.Println("\nUsage:")
fmt.Printf(" %v %v\n", cmdName, docs.Usage)
}
if len(docs.Args) > 0 {
fmt.Println("\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(os.Stdout, 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.Println("\nDetails:")
lines := strings.Split(docs.Detailed, "\n")
trimLeft := 0
if len(lines) == 0 {
return nil, 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.Printf(" %v\n", trimSpaceLeftUpto(line, trimLeft))
}
} }
} }
return NoResults{}, nil
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 { func trimSpaceLeftUpto(s string, n int) string {

View file

@ -18,7 +18,7 @@ func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error {
return err return err
} }
return r.displayResult(ctx, os.Stdout, res, false) return r.displayResult(ctx, r.inst.Out(), res, false)
} }
func (r *REPL) echoPrinter(ctx context.Context, w io.Writer, args []any) (err error) { func (r *REPL) echoPrinter(ctx context.Context, w io.Writer, args []any) (err error) {
@ -89,23 +89,25 @@ func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise
fmt.Fprintf(w, "]") fmt.Fprintf(w, "]")
} else { } else {
// In the off-chance that this is actually a slice of printables // In the off-chance that this is actually a slice of printables
vt := reflect.SliceOf(reflect.TypeOf(v[0])) if len(v) > 0 {
if tp, ok := r.typePrinters[vt]; ok { vt := reflect.SliceOf(reflect.TypeOf(v[0]))
canDisplay := true if tp, ok := r.typePrinters[vt]; ok {
canDisplay := true
typeSlice := reflect.MakeSlice(vt, len(v), len(v)) typeSlice := reflect.MakeSlice(vt, len(v), len(v))
for i := 0; i < len(v); i++ { for i := 0; i < len(v); i++ {
vv := reflect.ValueOf(v[i]) vv := reflect.ValueOf(v[i])
if vv.CanConvert(vt.Elem()) { if vv.CanConvert(vt.Elem()) {
typeSlice.Index(i).Set(vv) typeSlice.Index(i).Set(vv)
} else { } else {
canDisplay = false canDisplay = false
break break
}
} }
}
if canDisplay { if canDisplay {
return tp(w, typeSlice.Interface(), concise) return tp(w, typeSlice.Interface(), concise)
}
} }
} }

View file

@ -28,17 +28,17 @@ func New(opts ...ucl.InstOption) *REPL {
r.inst = ucl.New(instOpts...) r.inst = ucl.New(instOpts...)
r.SetCommand("help", r.helpBuiltin, Doc{ r.SetCommand("help", r.helpBuiltin, Doc{
Brief: "displays help about a command", Brief: "displays help about a command or topic",
Usage: "[command]", Usage: "[topic]",
Args: []ArgDoc{ Args: []ArgDoc{
{Name: "command", Brief: "command to display detailed help for"}, {Name: "topic", Brief: "topic to display"},
}, },
Detailed: ` Detailed: `
When used without arguments, 'help' will display the list of known commands, When used without arguments, 'help' will display the list of known commands,
along with a brief description on what each one does. along with a brief description on what each one does.
When used with an argument, 'help' will display a more detailed explanation If <topic> is a direct match, the contents of <topic> will be displayed.
of what each command does. Otherwise, 'help' will list the topics names that match the given substring.
`, `,
}) })