diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/jsonui.iml b/.idea/jsonui.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/jsonui.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b089683 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa64f0..f815124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.0 2021.06.11 + - Moved cmd to jsonui-cmd + - Made package out of jsonui, enabling others with json files to use + Easy: just run jsonui.Interactive([]byte with json.) ## 1.0.1 2018.09.15 - fixes `null` rendering - adds binary builds diff --git a/README.md b/README.md index 9d28355..e735755 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,42 @@ ![](img/jsonui.gif) +## Import into your application +jsonui can now be imported into your application -- +``` +... +jsonBytes,err := ioutil.ReadFile("jsonfile.json") +errorhandle(err) +jsonpath:=jsonui.Interactive(jsonBytes) +fmt.Println(jsonpath) +... +``` +When it runs, scroll down to the section, press x to execute. The output will look like this: +``` +] $ cat test.json | jsonui +JSON Path: address.gateways +``` +And then the user will be able to extract json using another utility called 'jj' +``` +]$ cat test.json | jj address.gateways +["Sopron", "Vienna", "Budapest"] +``` + +You could also use the jsonpath variable above for other packages like gjson or sjson to get / set values. ## Install +This Version: +`git clone https://github.com/rmasci/jsonui.git` +cd jsonui/jsonui-cmd +make + +Original: `go get -u github.com/gulyasm/jsonui` ## Binary Releases +This Version: +[Binary releases are availabe](https://github.com/rmasci/jsonui/releases) + +Original: [Binary releases are availabe](https://github.com/gulyasm/jsonui/releases) ## Usage @@ -42,11 +74,14 @@ Expand all nodes #### `C` Collapse all nodes - +### `x` +Execute. Will give you the JSON Path to use with jj command. +[https://github.com/tidwall/jj](https://github.com/tidwall/jj) #### `q/Ctrl+C` Quit jsonui + ## Acknowledgments Special thanks for [asciimoo](https://github.com/asciimoo) and the [wuzz](https://github.com/asciimoo/wuzz) project for all the help and suggestions. diff --git a/jsonui-cmd/Makefile b/jsonui-cmd/Makefile new file mode 100644 index 0000000..6bed02c --- /dev/null +++ b/jsonui-cmd/Makefile @@ -0,0 +1,33 @@ +compilerFlag=-gcflags=-trimpath=$(shell pwd) -asmflags=-trimpath=$(shell pwd) +goFiles=main.go +gomod=off +all: mac linux windows freebsd netbsd openbsd pi + +windows:$(goFiles) + GO111MODULE=$(gomod) GOOS=windows GOARCH=386 go build $(compilerFlag) -o ../../jsonui-release/win32-jsonui.exe $(goFiles) + GO111MODULE=$(gomod) GOOS=windows GOARCH=amd64 go build $(compilerFlag) -o ../../jsonui-release/win64-jsonui.exe $(goFiles) + +linux: $(goFiles) + GO111MODULE=$(gomod) GOOS=linux GOARCH=386 go build $(compilerFlag) -o ../../jsonui-release/lin32-jsonui $(goFiles) + GO111MODULE=$(gomod) GOOS=linux GOARCH=amd64 go build $(compilerFlag) -o ../../jsonui-release/lin64-jsonui $(goFiles) + +pi: $(gofiles) + GO111MODULE=$(gomod) GOOS=linux GOARCH=arm GOARM=6 go build $(compilerFlag) -o ../../jsonui-release/jsonui-pi $(goFilesU) + +mac: + GO111MODULE=$(gomod) GOOS=darwin GOARCH=amd64 go build $(compilerFlag) -o ../../jsonui-release/mac-jsonui $(goFiles) + +freebsd: $(goFiles) + GO111MODULE=$(gomod) GOOS=freebsd GOARCH=386 go build $(compilerFlag) -o ../../jsonui-release/freebsd32-jsonui $(goFiles) + GO111MODULE=$(gomod) GOOS=freebsd GOARCH=amd64 go build $(compilerFlag) -o ../../jsonui-release/freebsd64-jsonui $(goFiles) + +netbsd: $(goFiles) + GO111MODULE=$(gomod) GOOS=freebsd GOARCH=386 go build $(compilerFlag) -o ../../jsonui-release/netbsd32-jsonui $(goFiles) + GO111MODULE=$(gomod) GOOS=freebsd GOARCH=amd64 go build $(compilerFlag) -o ../../jsonui-release/netbsd64-jsonui $(goFiles) + +openbsd: $(goFiles) + GO111MODULE=$(gomod) GOOS=openbsd GOARCH=386 go build $(compilerFlag) -o ../../jsonui-release/openbsd32-jsonui $(goFiles) + GO111MODULE=$(gomod) GOOS=openbsd GOARCH=amd64 go build $(compilerFlag) -o ../../jsonui-release/openbsd64-jsonui $(goFiles) + +solaris: $(goFiles) + GO111MODULE=$(gomod) GOOS=solaris GOARCH=amd64 go build $(compilerFlag) -o ../../jsonui-release/sol-jsonui $(goFiles) \ No newline at end of file diff --git a/jsonui-cmd/main.go b/jsonui-cmd/main.go new file mode 100644 index 0000000..121ff1b --- /dev/null +++ b/jsonui-cmd/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/rmasci/jsonui" + "github.com/spf13/pflag" +) + +func main() { + var inFile string + var jsonByte []byte + var err error + pflag.StringVarP(&inFile, "file", "f", "", "JSON File to parse") + pflag.Parse() + if inFile != "" { + jsonByte, err = ioutil.ReadFile(inFile) + if err != nil { + fmt.Println("Could not read file", err) + os.Exit(1) + } + } else { + stat, _ := os.Stdin.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + jsonByte, err = ioutil.ReadAll(os.Stdin) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + } + if len(jsonByte) <= 1 { + fmt.Println("No Data") + os.Exit(1) + } + jp := jsonui.Interactive(jsonByte) + fmt.Println("JSON Path:", jp) +} diff --git a/jsonuipkg.go b/jsonuipkg.go new file mode 100644 index 0000000..0bc507e --- /dev/null +++ b/jsonuipkg.go @@ -0,0 +1,390 @@ +package jsonui + +// Most of the code written here is from a program called jsonui https://github.com/gulyasm/jsonui -- I did not write this +// but modified it to work in xjconvert. -- rx7322. + +import ( + "fmt" + "io/ioutil" + "log" + "math" + "strings" + + "github.com/jroimartin/gocui" +) + +//const VERSION = "1.0.1" + +const ( + treeView = "Hit 'h' for help, 'x' execute." + textView = "text" + pathView = "path" + helpView = "help" +) + +type position struct { + prc float32 + margin int +} + +func (p position) getCoordinate(max int) int { + // value = prc * MAX + abs + return int(p.prc*float32(max)) - p.margin +} + +type viewPosition struct { + x0, y0, x1, y1 position +} + +func logFile(s string) error { + d1 := []byte(s + "\n") + return ioutil.WriteFile("log.txt", d1, 0644) +} + +func (vp viewPosition) getCoordinates(maxX, maxY int) (int, int, int, int) { + var x0 = vp.x0.getCoordinate(maxX) + var y0 = vp.y0.getCoordinate(maxY) + var x1 = vp.x1.getCoordinate(maxX) + var y1 = vp.y1.getCoordinate(maxY) + return x0, y0, x1, y1 +} + +var helpWindowToggle = false + +var viewPositions = map[string]viewPosition{ + treeView: { + position{0.0, 0}, + position{0.0, 0}, + position{0.3, 2}, + position{0.9, 2}, + }, + textView: { + position{0.3, 0}, + position{0.0, 0}, + position{1.0, 2}, + position{0.9, 2}, + }, + pathView: { + position{0.0, 0}, + position{0.89, 0}, + position{1.0, 2}, + position{1.0, 2}, + }, +} + +var tree treeNode +// Very simple to use, just call jsonui.Interactive([]jsonbytes) and you enter in to interactive mode. The return is the location the user selected. From here the user can then use gjson to pull the value, display it to the user, or use it for other purposes. +func Interactive(jsonBod []byte) (jp string) { + jp = startInteract(jsonBod) + + //fmt.Printf("jsonPath=%v\n", jp) + //fmt.Printf("JSONPath: %v\n", jp) + return jp +} +func startInteract(jsonBod []byte) (jp string) { + var err error + tree, err = fromBytes(jsonBod) + if err != nil { + log.Panicln(err) + } + g, err := gocui.NewGui(gocui.OutputNormal) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + g.SetManagerFunc(layout) + + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding("", 'x', gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(treeView, 'k', gocui.ModNone, cursorMovement(-1)); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, 'j', gocui.ModNone, cursorMovement(1)); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, gocui.KeyArrowUp, gocui.ModNone, cursorMovement(-1)); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, gocui.KeyArrowDown, gocui.ModNone, cursorMovement(1)); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, 'K', gocui.ModNone, cursorMovement(-15)); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, 'J', gocui.ModNone, cursorMovement(15)); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, gocui.KeyPgup, gocui.ModNone, cursorMovement(-15)); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, gocui.KeyPgdn, gocui.ModNone, cursorMovement(15)); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, 'e', gocui.ModNone, toggleExpand); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, 'E', gocui.ModNone, expandAll); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(treeView, 'C', gocui.ModNone, collapseAll); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding("", 'h', gocui.ModNone, toggleHelp); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding("", '?', gocui.ModNone, toggleHelp); err != nil { + log.Panicln(err) + } + g.SelFgColor = gocui.ColorBlack + g.SelBgColor = gocui.ColorGreen + + if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } + p := findTreePosition(g) + for i, j := range p { + j = strings.TrimLeft(j, "[") + j = strings.TrimRight(j, "]") + if i == 0 { + jp = j + } else { + jp = jp + "." + j + } + } + log.Printf("Returning jp: %v\n", jp) + return jp +} + +const helpMessage = ` +Interactive - Help +---------------------------------------------- +j/ArrowDown ═ Move a line down +k/ArrowUp ═ Move a line up +J/PageDown ═ Move 15 line down +K/PageUp ═ Move 15 line up +e ═ Toggle expend/collapse node +E ═ Expand all nodes +C ═ Collapse all nodes +x/ctrl+c ═ Execute +h/? ═ Toggle help message +` + +func layout(g *gocui.Gui) error { + var views = []string{treeView, textView, pathView} + maxX, maxY := g.Size() + for _, view := range views { + x0, y0, x1, y1 := viewPositions[view].getCoordinates(maxX, maxY) + if v, err := g.SetView(view, x0, y0, x1, y1); err != nil { + v.SelFgColor = gocui.ColorBlack + v.SelBgColor = gocui.ColorGreen + + v.Title = " " + view + " " + if err != gocui.ErrUnknownView { + return err + + } + if v.Name() == treeView { + v.Highlight = true + drawTree(g, v, tree) + // v.Autoscroll = true + } + if v.Name() == textView { + drawJSON(g, v) + } + + } + } + if helpWindowToggle { + height := strings.Count(helpMessage, "\n") + 1 + width := -1 + for _, line := range strings.Split(helpMessage, "\n") { + width = int(math.Max(float64(width), float64(len(line)+2))) + } + if v, err := g.SetView(helpView, maxX/2-width/2, maxY/2-height/2, maxX/2+width/2, maxY/2+height/2); err != nil { + if err != gocui.ErrUnknownView { + return err + + } + fmt.Fprintln(v, helpMessage) + + } + } else { + g.DeleteView(helpView) + } + _, err := g.SetCurrentView(treeView) + if err != nil { + log.Fatal("failed to set current view: ", err) + } + return nil + +} +func getPath(g *gocui.Gui, v *gocui.View) string { + p := findTreePosition(g) + for i, s := range p { + transformed := s + if !strings.HasPrefix(s, "[") && !strings.HasSuffix(s, "]") { + transformed = fmt.Sprintf("[%q]", s) + } + p[i] = transformed + } + return strings.Join(p, "") +} + +func drawPath(g *gocui.Gui, v *gocui.View) error { + pv, err := g.View(pathView) + if err != nil { + log.Fatal("failed to get pathView", err) + } + p := getPath(g, v) + pv.Clear() + fmt.Fprintf(pv, p) + return nil +} +func drawJSON(g *gocui.Gui, v *gocui.View) error { + dv, err := g.View(textView) + if err != nil { + log.Fatal("failed to get textView", err) + } + p := findTreePosition(g) + treeTodraw := tree.find(p) + if treeTodraw != nil { + dv.Clear() + fmt.Fprintf(dv, treeTodraw.String(2, 0)) + } + return nil +} + +func lineBelow(v *gocui.View, d int) bool { + _, y := v.Cursor() + line, err := v.Line(y + d) + return err == nil && line != "" +} + +func countIndex(s string) int { + count := 0 + for _, c := range s { + if c == ' ' { + count++ + } + } + return count +} + +func getLine(s string, y int) string { + lines := strings.Split(s, "\n") + return lines[y] +} + +var cleanPatterns = []string{ + treeSignUpEnding, + treeSignDash, + treeSignUpMiddle, + treeSignVertical, + " (+)", +} + +func findTreePosition(g *gocui.Gui) treePosition { + v, err := g.View(treeView) + if err != nil { + log.Fatal("failed to get treeview", err) + } + path := treePosition{} + ci := -1 + _, yOffset := v.Origin() + _, yCurrent := v.Cursor() + y := yOffset + yCurrent + s := v.Buffer() + for cy := y; cy >= 0; cy-- { + line := getLine(s, cy) + for _, pattern := range cleanPatterns { + line = strings.Replace(line, pattern, "", -1) + } + + if count := countIndex(line); count < ci || ci == -1 { + path = append(path, strings.TrimSpace(line)) + ci = count + } + } + for i := len(path)/2 - 1; i >= 0; i-- { + opp := len(path) - 1 - i + path[i], path[opp] = path[opp], path[i] + } + + return path[1:] +} + +// This is a workaround for not having a Buffer +// function in gocui +func bufferLen(v *gocui.View) int { + s := v.Buffer() + return len(strings.Split(s, "\n")) - 1 +} + +func drawTree(g *gocui.Gui, v *gocui.View, tree treeNode) error { + tv, err := g.View(treeView) + if err != nil { + log.Fatal("failed to get treeView", err) + } + tv.Clear() + tree.draw(tv, 2, 0) + maxY := bufferLen(tv) + cx, cy := tv.Cursor() + lastLine := maxY - 2 + if cy > lastLine { + tv.SetCursor(cx, lastLine) + tv.SetOrigin(0, 0) + } + + return nil +} + +func expandAll(g *gocui.Gui, v *gocui.View) error { + tree.expandAll() + return drawTree(g, v, tree) +} + +func collapseAll(g *gocui.Gui, v *gocui.View) error { + tree.collapseAll() + return drawTree(g, v, tree) +} + +func toggleExpand(g *gocui.Gui, v *gocui.View) error { + p := findTreePosition(g) + subTree := tree.find(p) + subTree.toggleExpanded() + return drawTree(g, v, tree) +} + +func cursorMovement(d int) func(g *gocui.Gui, v *gocui.View) error { + return func(g *gocui.Gui, v *gocui.View) error { + dir := 1 + if d < 0 { + dir = -1 + } + distance := int(math.Abs(float64(d))) + for ; distance > 0; distance-- { + if lineBelow(v, distance*dir) { + v.MoveCursor(0, distance*dir, false) + drawJSON(g, v) + drawPath(g, v) + return nil + } + } + return nil + } +} +func toggleHelp(g *gocui.Gui, v *gocui.View) error { + helpWindowToggle = !helpWindowToggle + return nil +} + +func quit(g *gocui.Gui, v *gocui.View) error { + + return gocui.ErrQuit +} diff --git a/jsonui.go b/orig/jsonui.go similarity index 100% rename from jsonui.go rename to orig/jsonui.go diff --git a/tree_test.go b/orig/tree_test.go similarity index 100% rename from tree_test.go rename to orig/tree_test.go diff --git a/tree.go b/tree.go index 2515a11..d60151c 100644 --- a/tree.go +++ b/tree.go @@ -1,4 +1,4 @@ -package main +package jsonui import ( "encoding/json" diff --git a/tree_search_test.go b/tree_search_test.go index 6c53d12..24d24e4 100644 --- a/tree_search_test.go +++ b/tree_search_test.go @@ -1,4 +1,4 @@ -package main +package jsonui import "testing"