diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index 1688d2d..11bd913 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "github.com/chzyer/readline" "log" "ucl.lmika.dev/repl" @@ -22,13 +23,24 @@ func main() { ) ctx := context.Background() + instRepl.SetCommand("hello", func(ctx context.Context, args ucl.CallArgs) (any, error) { + fmt.Println("hello") + return nil, nil + }, repl.Doc{ + Brief: "displays hello", + Detailed: ` + This displays the message 'hello' to the terminal. + It then terminates. + `, + }) + for { line, err := rl.Readline() if err != nil { // io.EOF break } - if err := ucl.EvalAndDisplay(ctx, inst, line); err != nil { + if err := instRepl.EvalAndDisplay(ctx, line); err != nil { log.Printf("%T: %v", err, err) } } diff --git a/repl/docs.go b/repl/docs.go index a8ba0e4..b3ea160 100644 --- a/repl/docs.go +++ b/repl/docs.go @@ -1,6 +1,93 @@ package repl +import ( + "context" + "errors" + "fmt" + "github.com/lmika/gopkgs/fp/maps" + "os" + "sort" + "strings" + "text/tabwriter" + "ucl.lmika.dev/ucl" + "unicode" +) + type Doc struct { Brief string 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) { + switch { + case args.NArgs() == 0: + names := maps.Keys(r.commandDocs) + sort.Strings(names) + + tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + + for _, name := range names { + 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() + default: + 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.Detailed != "" { + fmt.Println("") + fmt.Println("Details:") + + 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 +} + +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 +} diff --git a/repl/evaldisplay.go b/repl/evaldisplay.go index c8704ec..8b1abb5 100644 --- a/repl/evaldisplay.go +++ b/repl/evaldisplay.go @@ -7,6 +7,8 @@ import ( "ucl.lmika.dev/ucl" ) +type NoResults struct{} + func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error { res, err := r.inst.Eval(ctx, expr) if err != nil { @@ -18,20 +20,26 @@ func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error { func displayResult(ctx context.Context, inst *ucl.Inst, res any) (err error) { switch v := res.(type) { + case NoResults: + return nil case nil: if _, err = fmt.Fprintln(os.Stdout, "(nil)"); err != nil { return err } - case listable: + case ucl.Listable: for i := 0; i < v.Len(); i++ { if err = displayResult(ctx, inst, v.Index(i)); err != nil { return err } } - default: + case ucl.Object: if _, err = fmt.Fprintln(os.Stdout, v.String()); err != nil { return err } + default: + if _, err = fmt.Fprintln(os.Stdout, v); err != nil { + return err + } } return nil } diff --git a/repl/repl.go b/repl/repl.go index ec872c1..387354d 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -1,10 +1,6 @@ package repl import ( - "context" - "fmt" - "github.com/lmika/gopkgs/fp/maps" - "sort" "ucl.lmika.dev/ucl" ) @@ -25,7 +21,16 @@ func New(opts ...ucl.InstOption) *REPL { inst: inst, commandDocs: make(map[string]Doc), } - inst.SetBuiltin("help", r.helpBuiltin) + r.SetCommand("help", r.helpBuiltin, Doc{ + Brief: "displays help about a command", + Detailed: ` + When used without arguments, 'help' will display the list of known commands, + along with a brief description on what each one does. + + When used with an argument, 'help' will display a more detailed explanation + of what each command does. + `, + }) return r } @@ -42,23 +47,3 @@ func (r *REPL) SetCommand(name string, fn ucl.BuiltinHandler, opts ...CommandOpt r.inst.SetBuiltin(name, fn) } - -func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) { - switch { - case args.NArgs() == 0: - // TEMP - names := maps.Keys(r.commandDocs) - sort.Strings(names) - - for _, name := range names { - cdoc := r.commandDocs[name] - if cdoc.Brief != "" { - fmt.Println("%v\t%v", name, r.commandDocs[name]) - } else { - fmt.Println("%v", name) - } - } - // END TEMP - } - return nil, nil -}