├── .circleci └── config.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── gui ├── gui.go ├── navi.go ├── tree.go └── type.go ├── lib └── edit.go ├── main.go └── test.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | build: 5 | parameters: 6 | go-version: 7 | type: string 8 | docker: 9 | - image: circleci/golang:<< parameters.go-version >> 10 | environment: 11 | GO111MODULE: "on" 12 | working_directory: /go/src/github.com/skanehira/tson 13 | 14 | commands: 15 | go_mod_download: 16 | steps: 17 | - restore_cache: 18 | name: Restore go modules cache 19 | keys: 20 | - go-modules-{{ checksum "go.sum" }} 21 | 22 | - run: go mod download 23 | 24 | - save_cache: 25 | name: Save go modules cache 26 | key: go-modules-{{ checksum "go.sum" }} 27 | paths: 28 | - "/go/pkg/mod" 29 | 30 | jobs: 31 | build: 32 | parameters: 33 | go-version: 34 | type: string 35 | 36 | executor: 37 | name: build 38 | go-version: << parameters.go-version >> 39 | 40 | steps: 41 | - checkout 42 | 43 | - go_mod_download 44 | 45 | lint: 46 | parameters: 47 | go-version: 48 | type: string 49 | golangci-lint-version: 50 | type: string 51 | 52 | executor: 53 | name: build 54 | go-version: << parameters.go-version >> 55 | 56 | steps: 57 | - checkout 58 | 59 | - go_mod_download 60 | 61 | - run: 62 | name: Install GolangCI-Lint 63 | command: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v<< parameters.golangci-lint-version >> 64 | 65 | - run: 66 | name: Run GolangCI-Lint 67 | command: ./bin/golangci-lint run --disable-all --enable=goimports --enable=govet 68 | 69 | release: 70 | parameters: 71 | go-version: 72 | type: string 73 | 74 | executor: 75 | name: build 76 | go-version: << parameters.go-version >> 77 | 78 | steps: 79 | - checkout 80 | - go_mod_download 81 | - run: 82 | name: Run goreleaser 83 | command: curl -sL https://git.io/goreleaser | bash -s -- --rm-dist 84 | 85 | workflows: 86 | stable-build: 87 | jobs: 88 | - lint: 89 | go-version: "1.13.3" 90 | golangci-lint-version: "1.17.1" 91 | - build: 92 | go-version: "1.13.3" 93 | requires: 94 | - lint 95 | 96 | latest-build: 97 | jobs: 98 | - lint: 99 | go-version: "1.13.3" 100 | golangci-lint-version: "1.17.1" 101 | - build: 102 | go-version: "1.13.3" 103 | requires: 104 | - lint 105 | 106 | release: 107 | jobs: 108 | - lint: 109 | go-version: "1.13.3" 110 | golangci-lint-version: "1.17.1" 111 | - release: 112 | go-version: "1.13.3" 113 | filters: 114 | branches: 115 | ignore: /.*/ 116 | tags: 117 | only: /[0-9]+(\.[0-9]+)(\.[0-9]+)/ 118 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: tson 2 | 3 | builds: 4 | - env: 5 | - CGO_ENABLED=0 6 | - GO111MODULE=on 7 | 8 | archives: 9 | - replacements: 10 | darwin: Darwin 11 | linux: Linux 12 | amd64: x86_64 13 | 14 | checksum: 15 | name_template: 'checksums.txt' 16 | 17 | snapshot: 18 | name_template: "{{ .Tag }}-next" 19 | 20 | changelog: 21 | skip: true 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 skanehira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is no longer to develop. 2 | # tson 3 | `tson` is JSON viewer and editor written in Go. 4 | This tool displays JSON as a tree and you can search and edit key or values. 5 | 6 | ![](https://i.imgur.com/9Z6qOY4.gif) 7 | 8 | ## Support OS 9 | - Mac 10 | - Linux 11 | 12 | ## Installation 13 | ```sh 14 | $ git clone https://github.com/skanehira/tson 15 | $ cd tson && go install 16 | ``` 17 | 18 | ## Usage 19 | ```sh 20 | # from file 21 | $ tson < test.json 22 | 23 | # from pipe 24 | $ curl -X POST http://gorilla/likes/regist | tson 25 | 26 | # from url(only can use http get mthod) 27 | $ tson -url http://gorilla/likes/json 28 | ``` 29 | 30 | ### Use `tson` as a library in your application 31 | You can use tson in your application as following. 32 | 33 | ```go 34 | package main 35 | 36 | import ( 37 | "fmt" 38 | 39 | tson "github.com/skanehira/tson/lib" 40 | ) 41 | 42 | func main() { 43 | j := []byte(`{"name":"gorilla"}`) 44 | 45 | // tson.Edit([]byte) will return []byte, error 46 | res, err := tson.Edit(j) 47 | if err != nil { 48 | fmt.Println(err) 49 | return 50 | } 51 | 52 | fmt.Println(string(res)) 53 | } 54 | ``` 55 | 56 | ## Keybinding 57 | ### JSON tree 58 | 59 | | key | description | 60 | |--------|--------------------------------| 61 | | j | move down | 62 | | k | move up | 63 | | g | move to the top | 64 | | G | move to the bottom | 65 | | ctrl-f | page up | 66 | | ctrl-b | page down | 67 | | h | hide current node | 68 | | H | collaspe value nodes | 69 | | l | expand current node | 70 | | L | expand all nodes | 71 | | r | read from file | 72 | | s | save to file | 73 | | a | add new node | 74 | | A | add new value | 75 | | d | clear children nodes | 76 | | e | edit json with $EDITOR | 77 | | q | quit tson | 78 | | Enter | edit node | 79 | | / or f | search nodes | 80 | | ? | show helps | 81 | | space | expand/collaspe children nodes | 82 | | ctrl-j | move to next parent node | 83 | | ctrk-k | move to next previous node | 84 | | ctrl-c | quit tson | 85 | 86 | ### help 87 | | key | description | 88 | |--------|--------------------| 89 | | j | move down | 90 | | k | move up | 91 | | g | move to the top | 92 | | G | move to the bottom | 93 | | ctrl-f | page up | 94 | | ctrl-b | page down | 95 | | q | close help | 96 | 97 | ## About editing nodes 98 | When editing a node value, the JSON value type is determined based on the value. 99 | For example, after inputed `10.5` and saving the JSON to a file, it will be output as a float type `10.5`. 100 | If the value sorround with `"`, it will be output as string type always. 101 | The following is a list of conversion rules. 102 | 103 | | input value | json type | 104 | |--------------------|-----------| 105 | | `gorilla` | string | 106 | | `10.5` | float | 107 | | `5` | int | 108 | | `true` or `false` | boolean | 109 | | `null` | null | 110 | | `"10"` or `"true"` | string | 111 | 112 | ## About adding new node 113 | You can use `a` to add new node with raw json string. 114 | 115 | For expample, you have following tree. 116 | 117 | ``` 118 | {array} <- your cursor in there 119 | ├──a 120 | ├──b 121 | └──c 122 | ``` 123 | 124 | If you input `{"name":"gorilla"}` and press add button, 125 | then you will get new tree as following. 126 | 127 | ``` 128 | {array} <- your cursor in there 129 | ├──a 130 | ├──b 131 | ├──c 132 | └──{object} 133 | └──name 134 | └──gorilla 135 | ``` 136 | 137 | Also, You can use `A` to add new value to current node. 138 | 139 | For example, you have following tree. 140 | 141 | ``` 142 | {object} <- your cursor in there 143 | └──name 144 | └──gorilla 145 | ``` 146 | 147 | If you input `{"age": 26}` and press add button, 148 | then you will get new tree as following. 149 | 150 | ``` 151 | {object} <- your cursor in there 152 | ├──name 153 | │ └──gorilla 154 | └──age 155 | └──26 156 | ``` 157 | 158 | # Author 159 | skanehira 160 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skanehira/tson 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/creack/pty v1.1.9 7 | github.com/gdamore/tcell v1.3.0 8 | github.com/gofrs/uuid v3.2.0+incompatible 9 | github.com/lucasb-eyer/go-colorful v1.0.3 // indirect 10 | github.com/mattn/go-runewidth v0.0.6 // indirect 11 | github.com/mitchellh/go-homedir v1.1.0 12 | github.com/rivo/tview v0.0.0-20191129065140-82b05c9fb329 13 | golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf 14 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= 2 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 3 | github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 6 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 7 | github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= 8 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 9 | github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= 10 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 11 | github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= 12 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= 13 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 14 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 15 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 16 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 17 | github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k= 18 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 19 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 20 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 21 | github.com/rivo/tview v0.0.0-20191129065140-82b05c9fb329 h1:MubHhHJ4mB0A5wMcc2am0/51RydztIDoumyOd0r0yBw= 22 | github.com/rivo/tview v0.0.0-20191129065140-82b05c9fb329/go.mod h1:/rBeY22VG2QprWnEqG57IBC8biVu3i0DOIjRLc9I8H0= 23 | github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 24 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf h1:fnPsqIDRbCSgumaMCRpoIoF2s4qxv0xSSS0BVZUE/ss= 27 | golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 28 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 31 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= 33 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20191018095205-727590c5006e h1:ZtoklVMHQy6BFRHkbG6JzK+S6rX82//Yeok1vMlizfQ= 35 | golang.org/x/sys v0.0.0-20191018095205-727590c5006e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= 37 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 41 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 42 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 43 | -------------------------------------------------------------------------------- /gui/gui.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "strconv" 15 | "strings" 16 | "syscall" 17 | 18 | "github.com/creack/pty" 19 | "github.com/gdamore/tcell" 20 | "github.com/rivo/tview" 21 | "golang.org/x/crypto/ssh/terminal" 22 | ) 23 | 24 | var ( 25 | ErrEmptyJSON = errors.New("empty json") 26 | ) 27 | 28 | type Gui struct { 29 | Tree *Tree 30 | Navi *Navi 31 | App *tview.Application 32 | Pages *tview.Pages 33 | } 34 | 35 | func New() *Gui { 36 | g := &Gui{ 37 | Tree: NewTree(), 38 | Navi: NewNavi(), 39 | App: tview.NewApplication(), 40 | Pages: tview.NewPages(), 41 | } 42 | return g 43 | } 44 | 45 | func (g *Gui) Run(i interface{}) error { 46 | g.Tree.UpdateView(g, i) 47 | g.Tree.SetKeybindings(g) 48 | g.Navi.UpdateView() 49 | g.Navi.SetKeybindings(g) 50 | 51 | grid := tview.NewGrid(). 52 | AddItem(g.Tree, 0, 0, 1, 1, 0, 0, true) 53 | 54 | g.Pages.AddAndSwitchToPage("main", grid, true) 55 | 56 | if err := g.App.SetRoot(g.Pages, true).Run(); err != nil { 57 | log.Println(err) 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (g *Gui) Modal(p tview.Primitive, width, height int) tview.Primitive { 65 | return tview.NewGrid(). 66 | SetColumns(0, width, 0). 67 | SetRows(0, height, 0). 68 | AddItem(p, 1, 1, 1, 1, 0, 0, true) 69 | } 70 | 71 | func (g *Gui) Message(message, page string, doneFunc func()) { 72 | doneLabel := "ok" 73 | modal := tview.NewModal(). 74 | SetText(message). 75 | AddButtons([]string{doneLabel}). 76 | SetDoneFunc(func(buttonIndex int, buttonLabel string) { 77 | g.Pages.RemovePage("message") 78 | g.Pages.SwitchToPage(page).ShowPage("main") 79 | if buttonLabel == doneLabel { 80 | doneFunc() 81 | } 82 | }) 83 | 84 | g.Pages.AddAndSwitchToPage("message", g.Modal(modal, 80, 29), true).ShowPage("main") 85 | } 86 | 87 | func (g *Gui) Input(text, label string, width int, doneFunc func(text string)) { 88 | input := tview.NewInputField().SetText(text) 89 | input.SetBorder(true) 90 | input.SetLabel(label).SetLabelWidth(width).SetDoneFunc(func(key tcell.Key) { 91 | if key == tcell.KeyEnter { 92 | doneFunc(input.GetText()) 93 | g.Pages.RemovePage("input") 94 | } 95 | }) 96 | 97 | g.Pages.AddAndSwitchToPage("input", g.Modal(input, 0, 3), true).ShowPage("main") 98 | } 99 | 100 | func (g *Gui) Form(fieldLabel []string, doneLabel, title, pageName string, 101 | height int, doneFunc func(values map[string]string) error) { 102 | 103 | if g.Pages.HasPage(pageName) { 104 | g.Pages.ShowPage(pageName) 105 | return 106 | } 107 | 108 | form := tview.NewForm() 109 | for _, label := range fieldLabel { 110 | form.AddInputField(label, "", 0, nil, nil) 111 | } 112 | 113 | form.AddButton(doneLabel, func() { 114 | values := make(map[string]string) 115 | 116 | for _, label := range fieldLabel { 117 | item := form.GetFormItemByLabel(label) 118 | switch item.(type) { 119 | case *tview.InputField: 120 | input, ok := item.(*tview.InputField) 121 | if ok { 122 | values[label] = os.ExpandEnv(input.GetText()) 123 | } 124 | } 125 | } 126 | 127 | if err := doneFunc(values); err != nil { 128 | g.Message(err.Error(), pageName, func() {}) 129 | return 130 | } 131 | 132 | g.Pages.RemovePage(pageName) 133 | }). 134 | AddButton("cancel", func() { 135 | g.Pages.RemovePage(pageName) 136 | }) 137 | 138 | form.SetBorder(true).SetTitle(title). 139 | SetTitleAlign(tview.AlignLeft) 140 | 141 | g.Pages.AddAndSwitchToPage(pageName, g.Modal(form, 0, height), true).ShowPage("main") 142 | } 143 | 144 | func (g *Gui) LoadJSON() { 145 | labels := []string{"file"} 146 | g.Form(labels, "read", "read from file", "read_from_file", 7, func(values map[string]string) error { 147 | fileName := values[labels[0]] 148 | file, err := os.Open(fileName) 149 | if err != nil { 150 | log.Println(fmt.Sprintf("can't open file: %s", err)) 151 | return err 152 | } 153 | defer file.Close() 154 | 155 | i, err := UnMarshalJSON(file) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | g.Tree.UpdateView(g, i) 161 | return nil 162 | }) 163 | } 164 | 165 | func (g *Gui) Search() { 166 | pageName := "search" 167 | if g.Pages.HasPage(pageName) { 168 | g.Pages.ShowPage(pageName) 169 | } else { 170 | input := tview.NewInputField() 171 | input.SetBorder(true).SetTitle("search").SetTitleAlign(tview.AlignLeft) 172 | input.SetChangedFunc(func(text string) { 173 | root := *g.Tree.OriginRoot 174 | g.Tree.SetRoot(&root) 175 | if text != "" { 176 | root := g.Tree.GetRoot() 177 | root.SetChildren(g.walk(root.GetChildren(), text)) 178 | } 179 | }) 180 | input.SetLabel("word").SetLabelWidth(5).SetDoneFunc(func(key tcell.Key) { 181 | if key == tcell.KeyEnter { 182 | g.Pages.HidePage(pageName) 183 | } 184 | }) 185 | 186 | g.Pages.AddAndSwitchToPage(pageName, g.Modal(input, 0, 3), true).ShowPage("main") 187 | } 188 | } 189 | 190 | func (g *Gui) walk(nodes []*tview.TreeNode, text string) []*tview.TreeNode { 191 | var newNodes []*tview.TreeNode 192 | 193 | for _, child := range nodes { 194 | log.Println(child.GetText()) 195 | if strings.Index(strings.ToLower(child.GetText()), strings.ToLower(text)) != -1 { 196 | newNodes = append(newNodes, child) 197 | } else { 198 | newNodes = append(newNodes, g.walk(child.GetChildren(), text)...) 199 | } 200 | } 201 | 202 | return newNodes 203 | } 204 | 205 | func (g *Gui) SaveJSON() { 206 | labels := []string{"file"} 207 | g.Form(labels, "save", "save to file", "save_to_file", 7, func(values map[string]string) error { 208 | file := values[labels[0]] 209 | file = os.ExpandEnv(file) 210 | return g.SaveJSONToFile(file) 211 | }) 212 | } 213 | 214 | func (g *Gui) SaveJSONToFile(file string) error { 215 | var buf bytes.Buffer 216 | enc := json.NewEncoder(&buf) 217 | enc.SetIndent("", " ") 218 | 219 | if err := enc.Encode(g.MakeJSON(g.Tree.OriginRoot)); err != nil { 220 | log.Println(fmt.Sprintf("can't marshal json: %s", err)) 221 | return err 222 | } 223 | 224 | if err := ioutil.WriteFile(file, buf.Bytes(), 0666); err != nil { 225 | log.Println(fmt.Sprintf("can't create file: %s", err)) 226 | return err 227 | } 228 | 229 | return nil 230 | } 231 | 232 | func (g *Gui) MakeJSON(node *tview.TreeNode) interface{} { 233 | ref := node.GetReference().(Reference) 234 | children := node.GetChildren() 235 | 236 | switch ref.JSONType { 237 | case Object: 238 | i := make(map[string]interface{}) 239 | for _, n := range children { 240 | i[n.GetText()] = g.MakeJSON(n) 241 | } 242 | return i 243 | case Array: 244 | var i []interface{} 245 | for _, n := range children { 246 | i = append(i, g.MakeJSON(n)) 247 | } 248 | return i 249 | case Key: 250 | if len(node.GetChildren()) == 0 { 251 | return "" 252 | } 253 | v := node.GetChildren()[0] 254 | if v.GetReference().(Reference).JSONType == Value { 255 | return g.parseValue(v) 256 | } 257 | return map[string]interface{}{ 258 | node.GetText(): g.MakeJSON(v), 259 | } 260 | } 261 | 262 | return g.parseValue(node) 263 | } 264 | 265 | func (g *Gui) parseValue(node *tview.TreeNode) interface{} { 266 | v := node.GetText() 267 | ref := node.GetReference().(Reference) 268 | 269 | switch ref.ValueType { 270 | case Int: 271 | i, _ := strconv.Atoi(v) 272 | return i 273 | case Float: 274 | f, _ := strconv.ParseFloat(v, 64) 275 | return f 276 | case Boolean: 277 | b, _ := strconv.ParseBool(v) 278 | return b 279 | case Null: 280 | return nil 281 | } 282 | 283 | return v 284 | } 285 | 286 | func (g *Gui) AddNode() { 287 | labels := []string{"json"} 288 | g.Form(labels, "add", "add new node", "add_new_node", 7, func(values map[string]string) error { 289 | j := values[labels[0]] 290 | if j == "" { 291 | log.Println(ErrEmptyJSON) 292 | return ErrEmptyJSON 293 | } 294 | 295 | buf := bytes.NewBufferString(j) 296 | i, err := UnMarshalJSON(buf) 297 | if err != nil { 298 | return err 299 | } 300 | 301 | newNode := NewRootTreeNode(i) 302 | newNode.SetChildren(g.Tree.AddNode(i)) 303 | g.Tree.GetCurrentNode().AddChild(newNode) 304 | // update new origin root node 305 | g.Tree.OriginRoot = g.Tree.GetRoot() 306 | 307 | return nil 308 | }) 309 | } 310 | 311 | func (g *Gui) AddValue() { 312 | labels := []string{"json"} 313 | g.Form(labels, "add", "add new value", "add_new_value", 7, func(values map[string]string) error { 314 | j := values[labels[0]] 315 | if j == "" { 316 | log.Println(ErrEmptyJSON) 317 | return ErrEmptyJSON 318 | } 319 | 320 | buf := bytes.NewBufferString(j) 321 | i, err := UnMarshalJSON(buf) 322 | if err != nil { 323 | return err 324 | } 325 | 326 | current := g.Tree.GetCurrentNode() 327 | for _, n := range g.Tree.AddNode(i) { 328 | current.AddChild(n) 329 | } 330 | // update new origin root node 331 | g.Tree.OriginRoot = g.Tree.GetRoot() 332 | 333 | return nil 334 | }) 335 | } 336 | 337 | func (g *Gui) NaviPanel() { 338 | if g.Pages.HasPage(NaviPageName) { 339 | g.Pages.ShowPage(NaviPageName) 340 | } else { 341 | g.Pages.AddAndSwitchToPage(NaviPageName, g.Modal(g.Navi, 0, 0), true).ShowPage("main") 342 | } 343 | } 344 | 345 | func (g *Gui) EditWithEditor() { 346 | g.App.Suspend(func() { 347 | f, err := ioutil.TempFile("", "tson") 348 | if err != nil { 349 | log.Println(fmt.Sprintf("can't create temp file: %s", err)) 350 | g.Message(err.Error(), "main", func() {}) 351 | return 352 | } 353 | f.Close() 354 | 355 | if err := g.SaveJSONToFile(f.Name()); err != nil { 356 | log.Println(fmt.Sprintf("can't write to temp file: %s", err)) 357 | g.Message(err.Error(), "main", func() {}) 358 | return 359 | } 360 | 361 | editor := os.Getenv("EDITOR") 362 | if editor == "" { 363 | msg := fmt.Sprint("$EDITOR is empty") 364 | log.Println(msg) 365 | g.Message(msg, "main", func() {}) 366 | return 367 | } 368 | 369 | var args []string 370 | if editor == "vim" { 371 | args = append(args, []string{"-c", "set ft=json", f.Name()}...) 372 | } else { 373 | args = append(args, f.Name()) 374 | } 375 | cmd := exec.Command(editor, args...) 376 | 377 | // Start the command with a pty. 378 | ptmx, err := pty.Start(cmd) 379 | if err != nil { 380 | g.Message(err.Error(), "main", func() {}) 381 | return 382 | } 383 | // Make sure to close the pty at the end. 384 | defer func() { 385 | if err := ptmx.Close(); err != nil { 386 | log.Printf("can't close pty: %s", err) 387 | } 388 | }() 389 | 390 | // Handle pty size. 391 | ch := make(chan os.Signal, 1) 392 | signal.Notify(ch, syscall.SIGWINCH) 393 | go func() { 394 | for range ch { 395 | if err := pty.InheritSize(os.Stdin, ptmx); err != nil { 396 | log.Printf("can't resizing pty: %s", err) 397 | } 398 | } 399 | }() 400 | ch <- syscall.SIGWINCH // Initial resize. 401 | 402 | // Set stdin in raw mode. 403 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 404 | if err != nil { 405 | log.Println(fmt.Sprintf("can't make terminal raw mode: %s", err)) 406 | g.Message(err.Error(), "main", func() {}) 407 | return 408 | } 409 | defer func() { 410 | if err := terminal.Restore(int(os.Stdin.Fd()), oldState); err != nil { 411 | log.Printf("can't restore terminal: %s", err) 412 | } 413 | }() 414 | 415 | // Copy stdin to the pty and the pty to stdout. 416 | go io.Copy(ptmx, os.Stdin) 417 | io.Copy(os.Stdout, ptmx) 418 | 419 | f, err = os.Open(f.Name()) 420 | if err != nil { 421 | log.Println(fmt.Sprintf("can't open file: %s", err)) 422 | g.Message(err.Error(), "main", func() {}) 423 | return 424 | } 425 | defer f.Close() 426 | 427 | i, err := UnMarshalJSON(f) 428 | if err != nil { 429 | log.Println(fmt.Sprintf("can't read from file: %s", err)) 430 | g.Message(err.Error(), "main", func() {}) 431 | return 432 | } 433 | 434 | os.RemoveAll(f.Name()) 435 | g.Tree.UpdateView(g, i) 436 | }) 437 | } 438 | 439 | func UnMarshalJSON(in io.Reader) (interface{}, error) { 440 | b, err := ioutil.ReadAll(in) 441 | if err != nil { 442 | log.Println(err) 443 | return nil, err 444 | } 445 | if len(b) == 0 { 446 | log.Println(err) 447 | return nil, ErrEmptyJSON 448 | } 449 | 450 | var i interface{} 451 | if err := json.Unmarshal(b, &i); err != nil { 452 | log.Println(err) 453 | return nil, err 454 | } 455 | 456 | return i, nil 457 | } 458 | -------------------------------------------------------------------------------- /gui/navi.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gdamore/tcell" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | var NaviPageName = "navi_panel" 12 | 13 | var RedColor = `[red::b]%s[white]: %s` 14 | 15 | // default keybinding 16 | var ( 17 | moveDown = fmt.Sprintf(RedColor, "j", " move down") 18 | moveUp = fmt.Sprintf(RedColor, "k", " move up") 19 | moveLeft = fmt.Sprintf(RedColor, "h", " move left") 20 | moveRight = fmt.Sprintf(RedColor, "l", " move right") 21 | moveTop = fmt.Sprintf(RedColor, "g", " move top") 22 | moveBottom = fmt.Sprintf(RedColor, "G", " move bottom") 23 | pageDown = fmt.Sprintf(RedColor, "ctrl-b", "page down") 24 | pageUp = fmt.Sprintf(RedColor, "ctrl-f", "page up") 25 | stopApp = fmt.Sprintf(RedColor, "ctrl-c", "stop tson") 26 | defaultNavi = strings.Join([]string{moveDown, moveUp, moveLeft, 27 | moveRight, moveTop, moveBottom, pageDown, pageUp, stopApp}, "\n") 28 | ) 29 | 30 | // tree keybinding 31 | var ( 32 | hideNode = fmt.Sprintf(RedColor, "h", " hide children nodes") 33 | collaspeAllNode = fmt.Sprintf(RedColor, "H", " collaspe all nodes") 34 | expandNode = fmt.Sprintf(RedColor, "l", " expand children nodes") 35 | expandAllNode = fmt.Sprintf(RedColor, "L", " expand all children nodes") 36 | readFile = fmt.Sprintf(RedColor, "r", " read from file") 37 | saveFile = fmt.Sprintf(RedColor, "s", " save to file") 38 | addNewNode = fmt.Sprintf(RedColor, "a", " add new node") 39 | addNewValue = fmt.Sprintf(RedColor, "A", " add new value") 40 | clearChildrenNodes = fmt.Sprintf(RedColor, "d", " clear children nodes") 41 | editNodes = fmt.Sprintf(RedColor, "e", " edit json with $EDITOR") 42 | quitTson = fmt.Sprintf(RedColor, "q", " quit tson") 43 | editNodeValue = fmt.Sprintf(RedColor, "Enter", "edit current node") 44 | searchNodes = fmt.Sprintf(RedColor, "/ or f", " search nodes") 45 | toggleExpandNodes = fmt.Sprintf(RedColor, "space", " expand/collaspe nodes") 46 | moveNextParentNode = fmt.Sprintf(RedColor, "ctrl-j", "move to next parent node") 47 | movePreParentNode = fmt.Sprintf(RedColor, "ctrl-k", "move to previous parent node") 48 | treeNavi = strings.Join([]string{hideNode, collaspeAllNode, expandNode, expandAllNode, 49 | readFile, saveFile, addNewNode, addNewValue, clearChildrenNodes, editNodeValue, searchNodes, 50 | moveNextParentNode, movePreParentNode, editNodes, quitTson}, "\n") 51 | ) 52 | 53 | type Navi struct { 54 | *tview.TextView 55 | } 56 | 57 | func NewNavi() *Navi { 58 | view := tview.NewTextView().SetDynamicColors(true) 59 | view.SetBorder(true).SetTitle("help").SetTitleAlign(tview.AlignLeft) 60 | navi := &Navi{TextView: view} 61 | return navi 62 | } 63 | 64 | func (n *Navi) UpdateView() { 65 | navi := strings.Join([]string{defaultNavi, "", treeNavi}, "\n") 66 | n.SetText(navi) 67 | } 68 | 69 | func (n *Navi) SetKeybindings(g *Gui) { 70 | n.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 71 | switch event.Rune() { 72 | case 'q': 73 | g.Pages.HidePage(NaviPageName) 74 | } 75 | 76 | return event 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /gui/tree.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/gdamore/tcell" 10 | "github.com/gofrs/uuid" 11 | "github.com/rivo/tview" 12 | ) 13 | 14 | const ( 15 | moveNext int = iota + 1 16 | movePre 17 | ) 18 | 19 | type Tree struct { 20 | *tview.TreeView 21 | OriginRoot *tview.TreeNode 22 | } 23 | 24 | func NewTree() *Tree { 25 | t := &Tree{ 26 | TreeView: tview.NewTreeView(), 27 | } 28 | 29 | t.SetBorder(true).SetTitle("json tree").SetTitleAlign(tview.AlignLeft) 30 | return t 31 | } 32 | 33 | func (t *Tree) UpdateView(g *Gui, i interface{}) { 34 | g.App.QueueUpdateDraw(func() { 35 | 36 | root := NewRootTreeNode(i) 37 | root.SetChildren(t.AddNode(i)) 38 | t.SetRoot(root).SetCurrentNode(root) 39 | 40 | originRoot := *root 41 | t.OriginRoot = &originRoot 42 | }) 43 | } 44 | 45 | func (t *Tree) AddNode(node interface{}) []*tview.TreeNode { 46 | var nodes []*tview.TreeNode 47 | 48 | switch node := node.(type) { 49 | case map[string]interface{}: 50 | for k, v := range node { 51 | newNode := t.NewNodeWithLiteral(k). 52 | SetColor(tcell.ColorMediumSlateBlue). 53 | SetChildren(t.AddNode(v)) 54 | r := reflect.ValueOf(v) 55 | 56 | id := uuid.Must(uuid.NewV4()).String() 57 | if r.Kind() == reflect.Slice { 58 | newNode.SetReference(Reference{ID: id, JSONType: Array}) 59 | } else if r.Kind() == reflect.Map { 60 | newNode.SetReference(Reference{ID: id, JSONType: Object}) 61 | } else { 62 | newNode.SetReference(Reference{ID: id, JSONType: Key}) 63 | } 64 | 65 | nodes = append(nodes, newNode) 66 | } 67 | case []interface{}: 68 | for _, v := range node { 69 | id := uuid.Must(uuid.NewV4()).String() 70 | switch v.(type) { 71 | case map[string]interface{}: 72 | objectNode := tview.NewTreeNode("{object}"). 73 | SetChildren(t.AddNode(v)).SetReference(Reference{ID: id, JSONType: Object}) 74 | nodes = append(nodes, objectNode) 75 | case []interface{}: 76 | arrayNode := tview.NewTreeNode("{array}"). 77 | SetChildren(t.AddNode(v)).SetReference(Reference{ID: id, JSONType: Array}) 78 | nodes = append(nodes, arrayNode) 79 | default: 80 | nodes = append(nodes, t.AddNode(v)...) 81 | } 82 | } 83 | default: 84 | ref := reflect.ValueOf(node) 85 | var valueType ValueType 86 | switch ref.Kind() { 87 | case reflect.Int: 88 | valueType = Int 89 | case reflect.Float64: 90 | valueType = Float 91 | case reflect.Bool: 92 | valueType = Boolean 93 | default: 94 | if node == nil { 95 | valueType = Null 96 | } else { 97 | valueType = String 98 | } 99 | } 100 | 101 | id := uuid.Must(uuid.NewV4()).String() 102 | nodes = append(nodes, t.NewNodeWithLiteral(node). 103 | SetReference(Reference{ID: id, JSONType: Value, ValueType: valueType})) 104 | } 105 | return nodes 106 | } 107 | 108 | func (t *Tree) NewNodeWithLiteral(i interface{}) *tview.TreeNode { 109 | if i == nil { 110 | return tview.NewTreeNode("null") 111 | } 112 | return tview.NewTreeNode(fmt.Sprintf("%v", i)) 113 | } 114 | 115 | func (t *Tree) SetKeybindings(g *Gui) { 116 | t.SetSelectedFunc(func(node *tview.TreeNode) { 117 | text := node.GetText() 118 | if text == "{object}" || text == "{array}" || text == "{value}" { 119 | return 120 | } 121 | labelWidth := 5 122 | g.Input(text, "text", labelWidth, func(text string) { 123 | ref := node.GetReference().(Reference) 124 | ref.ValueType = parseValueType(text) 125 | if ref.ValueType == String { 126 | text = strings.Trim(text, `"`) 127 | } 128 | node.SetText(text).SetReference(ref) 129 | }) 130 | }) 131 | 132 | t.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 133 | switch event.Rune() { 134 | case 'h': 135 | t.GetCurrentNode().SetExpanded(false) 136 | case 'H': 137 | t.CollapseValues(t.GetRoot()) 138 | case 'd': 139 | t.GetCurrentNode().ClearChildren() 140 | newRoot := *g.Tree.GetRoot() 141 | g.Tree.OriginRoot = &newRoot 142 | case 'L': 143 | t.GetRoot().ExpandAll() 144 | case 'l': 145 | t.GetCurrentNode().SetExpanded(true) 146 | case 'r': 147 | g.LoadJSON() 148 | case 's': 149 | g.SaveJSON() 150 | case '/', 'f': 151 | g.Search() 152 | case 'a': 153 | g.AddNode() 154 | case 'A': 155 | g.AddValue() 156 | case '?': 157 | g.NaviPanel() 158 | case 'e': 159 | g.EditWithEditor() 160 | case ' ': 161 | current := t.GetCurrentNode() 162 | current.SetExpanded(!current.IsExpanded()) 163 | case 'q': 164 | g.App.Stop() 165 | } 166 | 167 | switch event.Key() { 168 | case tcell.KeyCtrlJ: 169 | t.moveNode(moveNext) 170 | case tcell.KeyCtrlK: 171 | t.moveNode(movePre) 172 | } 173 | 174 | return event 175 | }) 176 | } 177 | 178 | func (t *Tree) CollapseValues(node *tview.TreeNode) { 179 | node.Walk(func(node, parent *tview.TreeNode) bool { 180 | ref := node.GetReference().(Reference) 181 | if ref.JSONType == Value { 182 | pRef := parent.GetReference().(Reference) 183 | t := pRef.JSONType 184 | if t == Key || t == Array { 185 | parent.SetExpanded(false) 186 | } 187 | } 188 | return true 189 | }) 190 | } 191 | 192 | func (t *Tree) moveNode(movement int) { 193 | current := t.GetCurrentNode() 194 | t.GetRoot().Walk(func(node, parent *tview.TreeNode) bool { 195 | if parent != nil { 196 | children := parent.GetChildren() 197 | for i, n := range children { 198 | if n.GetReference().(Reference).ID == current.GetReference().(Reference).ID { 199 | if movement == moveNext { 200 | if i < len(children)-1 { 201 | t.SetCurrentNode(children[i+1]) 202 | return false 203 | } 204 | } else if movement == movePre { 205 | if i > 0 { 206 | t.SetCurrentNode(children[i-1]) 207 | return false 208 | } 209 | } 210 | } 211 | } 212 | } 213 | 214 | return true 215 | }) 216 | } 217 | 218 | func parseValueType(text string) ValueType { 219 | // if sorround with `"` set string type 220 | if strings.HasPrefix(text, `"`) && strings.HasSuffix(text, `"`) { 221 | return String 222 | } else if "null" == text { 223 | return Null 224 | } else if text == "false" || text == "true" { 225 | return Boolean 226 | } else if _, err := strconv.ParseFloat(text, 64); err == nil { 227 | return Float 228 | } else if _, err := strconv.Atoi(text); err == nil { 229 | return Int 230 | } 231 | 232 | return String 233 | } 234 | 235 | func NewRootTreeNode(i interface{}) *tview.TreeNode { 236 | r := reflect.ValueOf(i) 237 | 238 | var root *tview.TreeNode 239 | switch r.Kind() { 240 | case reflect.Map: 241 | root = tview.NewTreeNode("{object}").SetReference(Reference{JSONType: Object}) 242 | case reflect.Slice: 243 | root = tview.NewTreeNode("{array}").SetReference(Reference{JSONType: Array}) 244 | default: 245 | root = tview.NewTreeNode("{value}").SetReference(Reference{JSONType: Key}) 246 | } 247 | return root 248 | } 249 | -------------------------------------------------------------------------------- /gui/type.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | type JSONType int 4 | 5 | const ( 6 | Root JSONType = iota + 1 7 | Object 8 | Array 9 | Key 10 | Value 11 | ) 12 | 13 | var jsonTypeMap = map[JSONType]string{ 14 | Object: "object", 15 | Array: "array", 16 | Key: "key", 17 | Value: "value", 18 | } 19 | 20 | func (t JSONType) String() string { 21 | return jsonTypeMap[t] 22 | } 23 | 24 | type ValueType int 25 | 26 | const ( 27 | Int ValueType = iota + 1 28 | String 29 | Float 30 | Boolean 31 | Null 32 | ) 33 | 34 | var valueTypeMap = map[ValueType]string{ 35 | Int: "int", 36 | String: "string", 37 | Float: "float", 38 | Boolean: "boolean", 39 | Null: "null", 40 | } 41 | 42 | func (v ValueType) String() string { 43 | return valueTypeMap[v] 44 | } 45 | 46 | type Reference struct { 47 | ID string 48 | JSONType JSONType 49 | ValueType ValueType 50 | } 51 | -------------------------------------------------------------------------------- /lib/edit.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | 8 | "github.com/skanehira/tson/gui" 9 | ) 10 | 11 | // Edit use tson as a library 12 | func Edit(b []byte) ([]byte, error) { 13 | // dont output log 14 | log.SetOutput(ioutil.Discard) 15 | 16 | var i interface{} 17 | if err := json.Unmarshal(b, &i); err != nil { 18 | log.Println(err) 19 | return nil, err 20 | } 21 | 22 | g := gui.New() 23 | if err := g.Run(i); err != nil { 24 | return nil, err 25 | } 26 | 27 | return json.Marshal(g.MakeJSON(g.Tree.GetRoot())) 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "syscall" 12 | 13 | "github.com/mitchellh/go-homedir" 14 | "github.com/skanehira/tson/gui" 15 | "golang.org/x/crypto/ssh/terminal" 16 | ) 17 | 18 | var ( 19 | enableLog = flag.Bool("log", false, "enable log") 20 | url = flag.String("url", "", "get json from url") 21 | ) 22 | 23 | func printError(err error) int { 24 | fmt.Fprintln(os.Stderr, err) 25 | return 1 26 | } 27 | 28 | func init() { 29 | flag.Parse() 30 | if *enableLog { 31 | home, err := homedir.Dir() 32 | if err != nil { 33 | fmt.Fprintln(os.Stderr, err) 34 | os.Exit(1) 35 | } 36 | 37 | logWriter, err := os.OpenFile(filepath.Join(home, "tson.log"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 38 | if err != nil { 39 | os.Exit(printError(err)) 40 | } 41 | 42 | log.SetOutput(logWriter) 43 | log.SetFlags(log.Lshortfile) 44 | } else { 45 | log.SetOutput(ioutil.Discard) 46 | } 47 | 48 | } 49 | 50 | func run() int { 51 | var i interface{} 52 | if *url != "" { 53 | resp, err := http.Get(*url) 54 | if err != nil { 55 | return printError(err) 56 | } 57 | defer resp.Body.Close() 58 | i, err = gui.UnMarshalJSON(resp.Body) 59 | if err != nil { 60 | return printError(err) 61 | } 62 | } else { 63 | if !terminal.IsTerminal(0) { 64 | var err error 65 | i, err = gui.UnMarshalJSON(os.Stdin) 66 | if err != nil { 67 | return printError(err) 68 | } 69 | 70 | // set tview tty to stdin 71 | os.Stdin = os.NewFile(uintptr(syscall.Stderr), "/dev/tty") 72 | } 73 | } 74 | 75 | if i == nil { 76 | return printError(gui.ErrEmptyJSON) 77 | } 78 | 79 | if err := gui.New().Run(i); err != nil { 80 | log.Println(err) 81 | return 1 82 | } 83 | return 0 84 | } 85 | 86 | func main() { 87 | os.Exit(run()) 88 | } 89 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5db6b6dd8021db8221a753fa", 4 | "index": 0, 5 | "guid": "b4bcb0d1-5b2d-4edd-8b05-daa3e1e2e95d", 6 | "isActive": true, 7 | "balance": "$2,672.05", 8 | "picture": "http://placehold.it/32x32", 9 | "age": 34, 10 | "eyeColor": "blue", 11 | "name": { 12 | "first": "Annmarie", 13 | "last": "Parsons" 14 | }, 15 | "company": "ENJOLA", 16 | "email": "annmarie.parsons@enjola.com", 17 | "phone": "+1 (902) 436-3940", 18 | "address": "460 Louis Place, Brookfield, Maine, 5135", 19 | "about": "Ut do nostrud do occaecat voluptate laborum in eu adipisicing et nisi. Do amet aliqua velit reprehenderit cupidatat ut labore exercitation voluptate dolore pariatur. Cillum ea fugiat duis veniam incididunt. Non minim commodo ipsum voluptate deserunt sint. Aliquip ex laborum aute nisi dolor adipisicing nisi excepteur commodo.", 20 | "registered": "Saturday, October 26, 2019 9:12 AM", 21 | "latitude": "17.735393", 22 | "longitude": "130.45959", 23 | "tags": [ 24 | "tempor", 25 | "ex", 26 | "reprehenderit", 27 | "do", 28 | "culpa" 29 | ], 30 | "range": [ 31 | 0, 32 | 1.2, 33 | 2.2, 34 | 3.2, 35 | 4, 36 | 5, 37 | 6, 38 | 7, 39 | 8, 40 | 9 41 | ], 42 | "friends": [ 43 | { 44 | "id": 0, 45 | "name": "Snyder Hayes" 46 | }, 47 | { 48 | "id": 1, 49 | "name": "Trisha Strong" 50 | }, 51 | { 52 | "id": 2, 53 | "name": "Margo Melton" 54 | } 55 | ], 56 | "greeting": "Hello, Annmarie! You have 5 unread messages.", 57 | "favoriteFruit": "strawberry" 58 | }, 59 | { 60 | "_id": "5db6b6dd66bc6efa94f855f6", 61 | "index": 1, 62 | "guid": "6ce96272-1435-403a-81b6-c7492c696676", 63 | "isActive": true, 64 | "balance": "$1,152.79", 65 | "picture": "http://placehold.it/32x32", 66 | "age": 22, 67 | "eyeColor": "green", 68 | "name": { 69 | "first": "Eileen", 70 | "last": "Dixon" 71 | }, 72 | "company": "DIGIPRINT", 73 | "email": "eileen.dixon@digiprint.name", 74 | "phone": "+1 (802) 599-2561", 75 | "address": "541 Cranberry Street, Westphalia, Vermont, 5884", 76 | "about": "Anim adipisicing ut cupidatat tempor in aliqua. Exercitation in pariatur eu dolor aliqua. Ad aute incididunt consectetur reprehenderit. Dolor aliqua cillum ullamco fugiat elit anim aliquip tempor. Id dolore amet mollit pariatur sunt. Non qui amet tempor mollit ex cillum.", 77 | "registered": "Monday, May 7, 2018 12:27 AM", 78 | "latitude": "78.861885", 79 | "longitude": "-133.172841", 80 | "tags": [ 81 | "minim", 82 | "pariatur", 83 | "aute", 84 | "consectetur", 85 | "voluptate" 86 | ], 87 | "range": [ 88 | 0, 89 | 1, 90 | 2, 91 | 3, 92 | 4, 93 | 5, 94 | 6, 95 | 7, 96 | 8, 97 | 9 98 | ], 99 | "friends": [ 100 | { 101 | "id": 0, 102 | "name": "Gena Mills" 103 | }, 104 | { 105 | "id": 1, 106 | "name": "Davidson Chan" 107 | }, 108 | { 109 | "id": 2, 110 | "name": "Queen Hanson" 111 | } 112 | ], 113 | "greeting": "Hello, Eileen! You have 9 unread messages.", 114 | "favoriteFruit": "apple" 115 | }, 116 | { 117 | "_id": "5db6b6ddd09cc04cd6f92160", 118 | "index": 2, 119 | "guid": "73987512-a994-4c9b-9ef3-827bd0dee48e", 120 | "isActive": true, 121 | "balance": "$2,167.31", 122 | "picture": "http://placehold.it/32x32", 123 | "age": 28, 124 | "eyeColor": "brown", 125 | "name": { 126 | "first": "Peggy", 127 | "last": "Medina" 128 | }, 129 | "company": "NIKUDA", 130 | "email": "peggy.medina@nikuda.us", 131 | "phone": "+1 (927) 426-2354", 132 | "address": "412 Robert Street, Libertytown, American Samoa, 2673", 133 | "about": "Qui veniam excepteur nisi aliquip velit consectetur deserunt nulla. Et proident mollit duis velit velit ex excepteur velit tempor magna est culpa. Sunt commodo id adipisicing eiusmod est occaecat deserunt magna occaecat cillum dolore eu duis labore. Sint reprehenderit commodo pariatur laborum amet aliquip sit exercitation aute magna velit minim cillum eiusmod.", 134 | "registered": "Saturday, October 6, 2018 7:32 AM", 135 | "latitude": "-17.706121", 136 | "longitude": "176.716276", 137 | "tags": [ 138 | "consequat", 139 | "amet", 140 | "duis", 141 | "Lorem", 142 | "velit" 143 | ], 144 | "range": [ 145 | 0, 146 | 1, 147 | 2, 148 | 3, 149 | 4, 150 | 5, 151 | 6, 152 | 7, 153 | 8, 154 | 9 155 | ], 156 | "friends": [ 157 | { 158 | "id": 0, 159 | "name": "Winnie Dotson" 160 | }, 161 | { 162 | "id": 1, 163 | "name": "Elisabeth Reilly" 164 | }, 165 | { 166 | "id": 2, 167 | "name": "Jordan Chambers" 168 | } 169 | ], 170 | "greeting": "Hello, Peggy! You have 9 unread messages.", 171 | "favoriteFruit": "apple" 172 | }, 173 | { 174 | "_id": "5db6b6dd2279ec9ae4ce1d24", 175 | "index": 3, 176 | "guid": "aa7ee3ea-0aa9-49c8-9b0c-e771ab704486", 177 | "isActive": false, 178 | "balance": "$1,337.52", 179 | "picture": "http://placehold.it/32x32", 180 | "age": 30, 181 | "eyeColor": "green", 182 | "name": { 183 | "first": "Delacruz", 184 | "last": "Duran" 185 | }, 186 | "company": "PANZENT", 187 | "email": "delacruz.duran@panzent.org", 188 | "phone": "+1 (856) 562-3584", 189 | "address": "396 Baltic Street, Salix, South Dakota, 7658", 190 | "about": "Ea proident deserunt quis aliquip consequat velit deserunt eu aliqua. Minim culpa enim laborum duis aute laborum quis eu esse aute aliquip. Voluptate ea qui ullamco aute ullamco officia laborum tempor minim.", 191 | "registered": "Sunday, February 7, 2016 1:32 PM", 192 | "latitude": "71.505958", 193 | "longitude": "142.168467", 194 | "tags": [ 195 | "anim", 196 | "dolor", 197 | "elit", 198 | "tempor", 199 | "ipsum" 200 | ], 201 | "range": [ 202 | 0, 203 | 1, 204 | 2, 205 | 3, 206 | 4, 207 | 5, 208 | 6, 209 | 7, 210 | 8, 211 | 9 212 | ], 213 | "friends": [ 214 | { 215 | "id": 0, 216 | "name": "Kathleen Mccarthy" 217 | }, 218 | { 219 | "id": 1, 220 | "name": "Jenkins Olson" 221 | }, 222 | { 223 | "id": 2, 224 | "name": "Garner Cherry" 225 | } 226 | ], 227 | "greeting": "Hello, Delacruz! You have 9 unread messages.", 228 | "favoriteFruit": "apple" 229 | }, 230 | { 231 | "_id": "5db6b6dd7faf14b79933ebb2", 232 | "index": 4, 233 | "guid": "11496d91-342e-4801-adfd-4bf1772ab084", 234 | "isActive": true, 235 | "balance": "$3,084.43", 236 | "picture": "http://placehold.it/32x32", 237 | "age": 22, 238 | "eyeColor": "brown", 239 | "name": { 240 | "first": "Travis", 241 | "last": "Wilkins" 242 | }, 243 | "company": "VIRXO", 244 | "email": "travis.wilkins@virxo.tv", 245 | "phone": "+1 (873) 467-3836", 246 | "address": "450 Powers Street, Walland, Northern Mariana Islands, 2027", 247 | "about": "Do reprehenderit anim sunt cillum. Labore dolore sit pariatur ut culpa occaecat sit excepteur sunt. Ad consequat adipisicing adipisicing consequat eu excepteur exercitation nostrud esse sit magna incididunt eu ullamco. Labore nostrud et consectetur ullamco nostrud qui eu eu. Elit voluptate consequat aliqua sint nulla aliqua reprehenderit non. Officia enim fugiat magna ullamco aute ipsum ipsum exercitation commodo mollit excepteur nostrud.", 248 | "registered": "Wednesday, June 3, 2015 7:04 AM", 249 | "latitude": "-39.212936", 250 | "longitude": "-80.925284", 251 | "tags": [ 252 | "magna", 253 | "amet", 254 | "veniam", 255 | "laboris", 256 | "elit" 257 | ], 258 | "range": [ 259 | 0, 260 | 1, 261 | 2, 262 | 3, 263 | 4, 264 | 5, 265 | 6, 266 | 7, 267 | 8, 268 | 9 269 | ], 270 | "friends": [ 271 | { 272 | "id": 0, 273 | "name": "Heath Maddox" 274 | }, 275 | { 276 | "id": 1, 277 | "name": "Jaclyn Riddle" 278 | }, 279 | { 280 | "id": 2, 281 | "name": "Bobbi Mclean" 282 | } 283 | ], 284 | "greeting": "Hello, Travis! You have 7 unread messages.", 285 | "favoriteFruit": "strawberry" 286 | }, 287 | { 288 | "_id": "5db6b6dd1a8b38305dc8f50f", 289 | "index": 5, 290 | "guid": "34fc0d3b-98be-4d32-932f-0c13dc5cf0d5", 291 | "isActive": true, 292 | "balance": "$2,520.61", 293 | "picture": "http://placehold.it/32x32", 294 | "age": 37, 295 | "eyeColor": "green", 296 | "name": { 297 | "first": "Reid", 298 | "last": "Klein" 299 | }, 300 | "company": "PLEXIA", 301 | "email": "reid.klein@plexia.io", 302 | "phone": "+1 (804) 518-2531", 303 | "address": "208 Irvington Place, Hegins, Alaska, 5145", 304 | "about": "Consectetur adipisicing sunt exercitation mollit ad proident mollit. Est veniam laborum Lorem officia. Consequat tempor tempor officia in consequat laboris quis labore nostrud excepteur deserunt occaecat est. Ut et officia dolore amet sunt eu excepteur dolore. Est officia culpa ipsum enim commodo consectetur nostrud dolore adipisicing.", 305 | "registered": "Thursday, August 7, 2014 10:01 PM", 306 | "latitude": "6.016204", 307 | "longitude": "156.529147", 308 | "tags": [ 309 | "ut", 310 | "pariatur", 311 | "proident", 312 | "cupidatat", 313 | "adipisicing" 314 | ], 315 | "range": [ 316 | 0, 317 | 1, 318 | 2, 319 | 3, 320 | 4, 321 | 5, 322 | 6, 323 | 7, 324 | 8, 325 | 9 326 | ], 327 | "friends": [ 328 | { 329 | "id": 0, 330 | "name": "Lydia Mendoza" 331 | }, 332 | { 333 | "id": 1, 334 | "name": "Hardin Baxter" 335 | }, 336 | { 337 | "id": 2, 338 | "name": "Arlene Dickerson" 339 | } 340 | ], 341 | "greeting": "Hello, Reid! You have 6 unread messages.", 342 | "favoriteFruit": "banana" 343 | }, 344 | { 345 | "_id": "5db6b6dde61399379778e82f", 346 | "index": 6, 347 | "guid": "08884257-7842-49c6-ae91-980727de4f45", 348 | "isActive": true, 349 | "balance": "$1,014.75", 350 | "picture": "http://placehold.it/32x32", 351 | "age": 32, 352 | "eyeColor": "brown", 353 | "name": { 354 | "first": "Michelle", 355 | "last": "Murray" 356 | }, 357 | "company": "COMBOGEN", 358 | "email": "michelle.murray@combogen.co.uk", 359 | "phone": "+1 (887) 532-3576", 360 | "address": "228 Orient Avenue, Winesburg, Minnesota, 7646", 361 | "about": "Esse magna amet exercitation sunt commodo non consectetur ad. Cillum non dolor irure nisi quis duis excepteur ullamco voluptate. Aliqua anim excepteur elit aute duis. Incididunt labore excepteur mollit duis non pariatur proident non cillum sint eiusmod in. Laboris amet aute commodo duis voluptate.", 362 | "registered": "Thursday, August 30, 2018 11:26 PM", 363 | "latitude": "74.476148", 364 | "longitude": "-52.719173", 365 | "tags": [ 366 | "tempor", 367 | "nulla", 368 | "cupidatat", 369 | "incididunt", 370 | "occaecat" 371 | ], 372 | "range": [ 373 | 0, 374 | 1, 375 | 2, 376 | 3, 377 | 4, 378 | 5, 379 | 6, 380 | 7, 381 | 8, 382 | 9 383 | ], 384 | "friends": [ 385 | { 386 | "id": 0, 387 | "name": "Gretchen Dorsey" 388 | }, 389 | { 390 | "id": 1, 391 | "name": "Sheryl Townsend" 392 | }, 393 | { 394 | "id": 2, 395 | "name": "Koch Stein" 396 | } 397 | ], 398 | "greeting": "Hello, Michelle! You have 5 unread messages.", 399 | "favoriteFruit": "apple" 400 | }, 401 | { 402 | "_id": "5db6b6dd607899c62e480b91", 403 | "index": 7, 404 | "guid": "f02d35be-d3f7-4d2c-87f5-245981f0d4cc", 405 | "isActive": true, 406 | "balance": "$3,066.88", 407 | "picture": "http://placehold.it/32x32", 408 | "age": 39, 409 | "eyeColor": "brown", 410 | "name": { 411 | "first": "Fletcher", 412 | "last": "Lindsay" 413 | }, 414 | "company": "VALREDA", 415 | "email": "fletcher.lindsay@valreda.me", 416 | "phone": "+1 (993) 464-3554", 417 | "address": "554 Bath Avenue, Dexter, Missouri, 9095", 418 | "about": "Magna velit voluptate officia nulla incididunt irure nostrud anim. Aliquip ullamco laboris reprehenderit fugiat consequat aliquip. Deserunt incididunt duis ullamco fugiat irure culpa incididunt sint occaecat Lorem et. Laboris officia qui aute et pariatur ad non aliqua minim aute excepteur.", 419 | "registered": "Friday, January 3, 2014 8:35 PM", 420 | "latitude": "-9.081936", 421 | "longitude": "18.298418", 422 | "tags": [ 423 | "Lorem", 424 | "tempor", 425 | "proident", 426 | "amet", 427 | "anim" 428 | ], 429 | "range": [ 430 | 0, 431 | 1, 432 | 2, 433 | 3, 434 | 4, 435 | 5, 436 | 6, 437 | 7, 438 | 8, 439 | 9 440 | ], 441 | "friends": [ 442 | { 443 | "id": 0, 444 | "name": "Woodard Merritt" 445 | }, 446 | { 447 | "id": 1, 448 | "name": "Rosetta Vincent" 449 | }, 450 | { 451 | "id": 2, 452 | "name": "Kennedy Rojas" 453 | } 454 | ], 455 | "greeting": "Hello, Fletcher! You have 6 unread messages.", 456 | "favoriteFruit": "strawberry" 457 | } 458 | ] 459 | --------------------------------------------------------------------------------