From ee2e9464a7e7fab70b3f0527b5327253cb4be6b1 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 11 Dec 2024 22:30:14 +1100 Subject: [PATCH] Added custom printers --- cmd/cmsh/fancy.go | 57 +++++++++++++++++++++++ cmd/cmsh/main.go | 5 ++ repl/evaldisplay.go | 108 ++++++++++++++++++++++++++++++++++++++++---- repl/repl.go | 18 ++++++-- repl/typeprinter.go | 20 ++++++++ ucl/builtins.go | 13 ++++++ ucl/inst.go | 9 ++++ 7 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 cmd/cmsh/fancy.go create mode 100644 repl/typeprinter.go diff --git a/cmd/cmsh/fancy.go b/cmd/cmsh/fancy.go new file mode 100644 index 0000000..e570954 --- /dev/null +++ b/cmd/cmsh/fancy.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "io" + "ucl.lmika.dev/repl" + "ucl.lmika.dev/ucl" +) + +type FancyType struct { + Foo string + Bar string +} + +var newFancyDoc = repl.Doc{ + Brief: "returns something fancy", + Detailed: ` + This will create and return something fancy. + `, +} + +func newFancy(ctx context.Context, args ucl.CallArgs) (any, error) { + return FancyType{Foo: "is foo", Bar: "is bar"}, nil +} + +func manyFancies(ctx context.Context, args ucl.CallArgs) (any, error) { + return []FancyType{ + {Foo: "foo 1", Bar: "bar 1"}, + {Foo: "foo 2", Bar: "bar 2"}, + {Foo: "foo 3", Bar: "bar 3"}, + }, nil +} + +func displayFancy(w io.Writer, f FancyType, concise bool) error { + if concise { + _, err := fmt.Fprintf(w, "%s:%s", f.Foo, f.Bar) + return err + } + + fmt.Fprintf(w, "Foo.. %s\n", f.Foo) + fmt.Fprintf(w, "Bar.. %s\n", f.Bar) + return nil +} + +func displayFancies(w io.Writer, fs []FancyType, concise bool) error { + if concise { + _, err := fmt.Fprintf(w, "%d fancies", len(fs)) + return err + } + + fmt.Fprintln(w, "FOO\tBAR") + for _, f := range fs { + fmt.Fprintf(w, "%v\t%v\n", f.Foo, f.Bar) + } + return nil +} diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index 11bd913..8793777 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -33,6 +33,11 @@ func main() { It then terminates. `, }) + instRepl.SetCommand("new-fancy", newFancy, newFancyDoc) + instRepl.SetCommand("many-fancies", manyFancies) + + repl.AddTypePrinter(instRepl, displayFancy) + repl.AddTypePrinter(instRepl, displayFancies) for { line, err := rl.Readline() diff --git a/repl/evaldisplay.go b/repl/evaldisplay.go index 8b1abb5..d062778 100644 --- a/repl/evaldisplay.go +++ b/repl/evaldisplay.go @@ -3,7 +3,10 @@ package repl import ( "context" "fmt" + "io" "os" + "reflect" + "strings" "ucl.lmika.dev/ucl" ) @@ -15,31 +18,118 @@ func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error { return err } - return displayResult(ctx, r.inst, res) + return r.displayResult(ctx, os.Stdout, res, false) } -func displayResult(ctx context.Context, inst *ucl.Inst, res any) (err error) { +func (r *REPL) echoPrinter(ctx context.Context, w io.Writer, args []any) (err error) { + if len(args) == 0 { + _, err := fmt.Fprintln(w) + return err + } + + var line strings.Builder + for _, arg := range args { + if err := r.displayResult(ctx, &line, arg, len(args) > 1); err != nil { + return err + } + } + + res := line.String() + if strings.HasSuffix(res, "\n") { + res = res[:len(res)-1] + } + + _, err = fmt.Fprintln(w, res) + return nil +} + +func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise bool) (err error) { + // Check type printers + tp, ok := r.typePrinters[reflect.TypeOf(res)] + if ok { + return tp(w, res, concise) + } + switch v := res.(type) { - case NoResults: - return nil case nil: if _, err = fmt.Fprintln(os.Stdout, "(nil)"); err != nil { return err } case ucl.Listable: - for i := 0; i < v.Len(); i++ { - if err = displayResult(ctx, inst, v.Index(i)); err != nil { - return err + if concise { + fmt.Fprintf(w, "[") + for i := 0; i < v.Len(); i++ { + if i > 0 { + fmt.Fprintf(w, " ") + } + if err = r.displayResult(ctx, w, v.Index(i), true); err != nil { + return err + } + } + fmt.Fprintf(w, "]") + } else { + for i := 0; i < v.Len(); i++ { + if err = r.displayResult(ctx, w, v.Index(i), true); err != nil { + return err + } + fmt.Fprintf(w, "\n") + } + } + case []interface{}: + if concise { + fmt.Fprintf(w, "[") + for i := 0; i < len(v); i++ { + if i > 0 { + fmt.Fprintf(w, " ") + } + if err = r.displayResult(ctx, w, v[i], true); err != nil { + return err + } + } + fmt.Fprintf(w, "]") + } else { + // In the off-chance that this is actually a slice of printables + vt := reflect.SliceOf(reflect.TypeOf(v[0])) + if tp, ok := r.typePrinters[vt]; ok { + canDisplay := true + + typeSlice := reflect.MakeSlice(vt, len(v), len(v)) + for i := 0; i < len(v); i++ { + vv := reflect.ValueOf(v[i]) + if vv.CanConvert(vt.Elem()) { + typeSlice.Index(i).Set(vv) + } else { + canDisplay = false + break + } + } + + if canDisplay { + return tp(w, typeSlice.Interface(), concise) + } + } + + for i := 0; i < len(v); i++ { + if err = r.displayResult(ctx, w, v[i], true); err != nil { + return err + } + fmt.Fprintf(w, "\n") } } case ucl.Object: - if _, err = fmt.Fprintln(os.Stdout, v.String()); err != nil { + if _, err = fmt.Fprint(w, v.String()); err != nil { return err } + if !concise { + fmt.Fprintln(w) + } default: - if _, err = fmt.Fprintln(os.Stdout, v); err != nil { + if _, err = fmt.Fprint(w, v); err != nil { return err } + if !concise { + fmt.Fprintln(w) + } } return nil } diff --git a/repl/repl.go b/repl/repl.go index b9cf19e..58997d4 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -1,6 +1,9 @@ package repl import ( + "io" + "reflect" + "slices" "ucl.lmika.dev/ucl" ) @@ -11,16 +14,19 @@ type CommandOpt interface { type REPL struct { inst *ucl.Inst - commandDocs map[string]Doc + commandDocs map[string]Doc + typePrinters map[reflect.Type]func(w io.Writer, v any, brief bool) error } func New(opts ...ucl.InstOption) *REPL { - inst := ucl.New(opts...) - r := &REPL{ - inst: inst, - commandDocs: make(map[string]Doc), + commandDocs: make(map[string]Doc), + typePrinters: make(map[reflect.Type]func(w io.Writer, v any, brief bool) error), } + + instOpts := append(slices.Clone(opts), ucl.WithCustomEchoPrinter(r.echoPrinter)) + r.inst = ucl.New(instOpts...) + r.SetCommand("help", r.helpBuiltin, Doc{ Brief: "displays help about a command", Usage: "[command]", @@ -36,6 +42,8 @@ func New(opts ...ucl.InstOption) *REPL { `, }) + AddTypePrinter(r, func(w io.Writer, t NoResults, concise bool) error { return nil }) + return r } diff --git a/repl/typeprinter.go b/repl/typeprinter.go new file mode 100644 index 0000000..d373e72 --- /dev/null +++ b/repl/typeprinter.go @@ -0,0 +1,20 @@ +package repl + +import ( + "fmt" + "io" + "reflect" +) + +func AddTypePrinter[T any](r *REPL, p func(w io.Writer, t T, concise bool) error) { + var t T + + tt := reflect.TypeOf(t) + r.typePrinters[tt] = func(w io.Writer, v any, concise bool) error { + vt, ok := v.(T) + if !ok { + return fmt.Errorf("cannot convert %v to T", v) + } + return p(w, vt, concise) + } +} diff --git a/ucl/builtins.go b/ucl/builtins.go index 371f2a3..791f9d5 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -9,6 +9,19 @@ import ( ) func echoBuiltin(ctx context.Context, args invocationArgs) (Object, error) { + echoPrinter := args.inst.echoPrinter + if echoPrinter != nil { + convertedArgs := make([]interface{}, len(args.args)) + for i, arg := range args.args { + if convArg, ok := toGoValue(arg); ok { + convertedArgs[i] = convArg + } else { + convertedArgs[i] = arg + } + } + return nil, echoPrinter(ctx, args.inst.out, convertedArgs) + } + if len(args.args) == 0 { if _, err := fmt.Fprintln(args.inst.Out()); err != nil { return nil, err diff --git a/ucl/inst.go b/ucl/inst.go index f1c9b19..a42376b 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -11,6 +11,7 @@ import ( type Inst struct { out io.Writer missingBuiltinHandler MissingBuiltinHandler + echoPrinter EchoPrinter rootEC *evalCtx } @@ -37,6 +38,14 @@ func WithModule(module Module) InstOption { } } +type EchoPrinter func(ctx context.Context, w io.Writer, args []any) error + +func WithCustomEchoPrinter(echoPrinter EchoPrinter) InstOption { + return func(i *Inst) { + i.echoPrinter = echoPrinter + } +} + type Module struct { Name string Builtins map[string]BuiltinHandler