package gui import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "os" "os/exec" "os/signal" "strconv" "strings" "syscall" "github.com/creack/pty" "github.com/gdamore/tcell" "github.com/rivo/tview" "golang.org/x/crypto/ssh/terminal" ) var ( ErrEmptyJSON = errors.New("empty json") ) type Gui struct { Tree *Tree Navi *Navi App *tview.Application Pages *tview.Pages } func New() *Gui { g := &Gui{ Tree: NewTree(), Navi: NewNavi(), App: tview.NewApplication(), Pages: tview.NewPages(), } return g } func (g *Gui) Run(i interface{}) error { g.Tree.UpdateView(g, i) g.Tree.SetKeybindings(g) g.Navi.UpdateView() g.Navi.SetKeybindings(g) grid := tview.NewGrid(). AddItem(g.Tree, 0, 0, 1, 1, 0, 0, true) g.Pages.AddAndSwitchToPage("main", grid, true) if err := g.App.SetRoot(g.Pages, true).Run(); err != nil { log.Println(err) return err } return nil } func (g *Gui) Modal(p tview.Primitive, width, height int) tview.Primitive { return tview.NewGrid(). SetColumns(0, width, 0). SetRows(0, height, 0). AddItem(p, 1, 1, 1, 1, 0, 0, true) } func (g *Gui) Message(message, page string, doneFunc func()) { doneLabel := "ok" modal := tview.NewModal(). SetText(message). AddButtons([]string{doneLabel}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { g.Pages.RemovePage("message") g.Pages.SwitchToPage(page).ShowPage("main") if buttonLabel == doneLabel { doneFunc() } }) g.Pages.AddAndSwitchToPage("message", g.Modal(modal, 80, 29), true).ShowPage("main") } func (g *Gui) Input(text, label string, width int, doneFunc func(text string)) { input := tview.NewInputField().SetText(text) input.SetBorder(true) input.SetLabel(label).SetLabelWidth(width).SetDoneFunc(func(key tcell.Key) { if key == tcell.KeyEnter { doneFunc(input.GetText()) g.Pages.RemovePage("input") } }) g.Pages.AddAndSwitchToPage("input", g.Modal(input, 0, 3), true).ShowPage("main") } func (g *Gui) Form(fieldLabel []string, doneLabel, title, pageName string, height int, doneFunc func(values map[string]string) error) { if g.Pages.HasPage(pageName) { g.Pages.ShowPage(pageName) return } form := tview.NewForm() for _, label := range fieldLabel { form.AddInputField(label, "", 0, nil, nil) } form.AddButton(doneLabel, func() { values := make(map[string]string) for _, label := range fieldLabel { item := form.GetFormItemByLabel(label) switch item.(type) { case *tview.InputField: input, ok := item.(*tview.InputField) if ok { values[label] = os.ExpandEnv(input.GetText()) } } } if err := doneFunc(values); err != nil { g.Message(err.Error(), pageName, func() {}) return } g.Pages.RemovePage(pageName) }). AddButton("cancel", func() { g.Pages.RemovePage(pageName) }) form.SetBorder(true).SetTitle(title). SetTitleAlign(tview.AlignLeft) g.Pages.AddAndSwitchToPage(pageName, g.Modal(form, 0, height), true).ShowPage("main") } func (g *Gui) LoadJSON() { labels := []string{"file"} g.Form(labels, "read", "read from file", "read_from_file", 7, func(values map[string]string) error { fileName := values[labels[0]] file, err := os.Open(fileName) if err != nil { log.Println(fmt.Sprintf("can't open file: %s", err)) return err } defer file.Close() i, err := UnMarshalJSON(file) if err != nil { return err } g.Tree.UpdateView(g, i) return nil }) } func (g *Gui) Search() { pageName := "search" if g.Pages.HasPage(pageName) { g.Pages.ShowPage(pageName) } else { input := tview.NewInputField() input.SetBorder(true).SetTitle("search").SetTitleAlign(tview.AlignLeft) input.SetChangedFunc(func(text string) { root := *g.Tree.OriginRoot g.Tree.SetRoot(&root) if text != "" { root := g.Tree.GetRoot() root.SetChildren(g.walk(root.GetChildren(), text)) } }) input.SetLabel("word").SetLabelWidth(5).SetDoneFunc(func(key tcell.Key) { if key == tcell.KeyEnter { g.Pages.HidePage(pageName) } }) g.Pages.AddAndSwitchToPage(pageName, g.Modal(input, 0, 3), true).ShowPage("main") } } func (g *Gui) walk(nodes []*tview.TreeNode, text string) []*tview.TreeNode { var newNodes []*tview.TreeNode for _, child := range nodes { log.Println(child.GetText()) if strings.Index(strings.ToLower(child.GetText()), strings.ToLower(text)) != -1 { newNodes = append(newNodes, child) } else { newNodes = append(newNodes, g.walk(child.GetChildren(), text)...) } } return newNodes } func (g *Gui) SaveJSON() { labels := []string{"file"} g.Form(labels, "save", "save to file", "save_to_file", 7, func(values map[string]string) error { file := values[labels[0]] file = os.ExpandEnv(file) return g.SaveJSONToFile(file) }) } func (g *Gui) SaveJSONToFile(file string) error { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetIndent("", " ") if err := enc.Encode(g.MakeJSON(g.Tree.OriginRoot)); err != nil { log.Println(fmt.Sprintf("can't marshal json: %s", err)) return err } if err := ioutil.WriteFile(file, buf.Bytes(), 0666); err != nil { log.Println(fmt.Sprintf("can't create file: %s", err)) return err } return nil } func (g *Gui) MakeJSON(node *tview.TreeNode) interface{} { ref := node.GetReference().(Reference) children := node.GetChildren() switch ref.JSONType { case Object: i := make(map[string]interface{}) for _, n := range children { i[n.GetText()] = g.MakeJSON(n) } return i case Array: var i []interface{} for _, n := range children { i = append(i, g.MakeJSON(n)) } return i case Key: if len(node.GetChildren()) == 0 { return "" } v := node.GetChildren()[0] if v.GetReference().(Reference).JSONType == Value { return g.parseValue(v) } return map[string]interface{}{ node.GetText(): g.MakeJSON(v), } } return g.parseValue(node) } func (g *Gui) parseValue(node *tview.TreeNode) interface{} { v := node.GetText() ref := node.GetReference().(Reference) switch ref.ValueType { case Int: i, _ := strconv.Atoi(v) return i case Float: f, _ := strconv.ParseFloat(v, 64) return f case Boolean: b, _ := strconv.ParseBool(v) return b case Null: return nil } return v } func (g *Gui) AddNode() { labels := []string{"json"} g.Form(labels, "add", "add new node", "add_new_node", 7, func(values map[string]string) error { j := values[labels[0]] if j == "" { log.Println(ErrEmptyJSON) return ErrEmptyJSON } buf := bytes.NewBufferString(j) i, err := UnMarshalJSON(buf) if err != nil { return err } newNode := NewRootTreeNode(i) newNode.SetChildren(g.Tree.AddNode(i)) g.Tree.GetCurrentNode().AddChild(newNode) // update new origin root node g.Tree.OriginRoot = g.Tree.GetRoot() return nil }) } func (g *Gui) AddValue() { labels := []string{"json"} g.Form(labels, "add", "add new value", "add_new_value", 7, func(values map[string]string) error { j := values[labels[0]] if j == "" { log.Println(ErrEmptyJSON) return ErrEmptyJSON } buf := bytes.NewBufferString(j) i, err := UnMarshalJSON(buf) if err != nil { return err } current := g.Tree.GetCurrentNode() for _, n := range g.Tree.AddNode(i) { current.AddChild(n) } // update new origin root node g.Tree.OriginRoot = g.Tree.GetRoot() return nil }) } func (g *Gui) NaviPanel() { if g.Pages.HasPage(NaviPageName) { g.Pages.ShowPage(NaviPageName) } else { g.Pages.AddAndSwitchToPage(NaviPageName, g.Modal(g.Navi, 0, 0), true).ShowPage("main") } } func (g *Gui) EditWithEditor() { g.App.Suspend(func() { f, err := ioutil.TempFile("", "tson") if err != nil { log.Println(fmt.Sprintf("can't create temp file: %s", err)) g.Message(err.Error(), "main", func() {}) return } f.Close() if err := g.SaveJSONToFile(f.Name()); err != nil { log.Println(fmt.Sprintf("can't write to temp file: %s", err)) g.Message(err.Error(), "main", func() {}) return } editor := os.Getenv("EDITOR") if editor == "" { msg := fmt.Sprint("$EDITOR is empty") log.Println(msg) g.Message(msg, "main", func() {}) return } var args []string if editor == "vim" { args = append(args, []string{"-c", "set ft=json", f.Name()}...) } else { args = append(args, f.Name()) } cmd := exec.Command(editor, args...) // Start the command with a pty. ptmx, err := pty.Start(cmd) if err != nil { g.Message(err.Error(), "main", func() {}) return } // Make sure to close the pty at the end. defer func() { if err := ptmx.Close(); err != nil { log.Printf("can't close pty: %s", err) } }() // Handle pty size. ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGWINCH) go func() { for range ch { if err := pty.InheritSize(os.Stdin, ptmx); err != nil { log.Printf("can't resizing pty: %s", err) } } }() ch <- syscall.SIGWINCH // Initial resize. // Set stdin in raw mode. oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) if err != nil { log.Println(fmt.Sprintf("can't make terminal raw mode: %s", err)) g.Message(err.Error(), "main", func() {}) return } defer func() { if err := terminal.Restore(int(os.Stdin.Fd()), oldState); err != nil { log.Printf("can't restore terminal: %s", err) } }() // Copy stdin to the pty and the pty to stdout. go io.Copy(ptmx, os.Stdin) io.Copy(os.Stdout, ptmx) f, err = os.Open(f.Name()) if err != nil { log.Println(fmt.Sprintf("can't open file: %s", err)) g.Message(err.Error(), "main", func() {}) return } defer f.Close() i, err := UnMarshalJSON(f) if err != nil { log.Println(fmt.Sprintf("can't read from file: %s", err)) g.Message(err.Error(), "main", func() {}) return } os.RemoveAll(f.Name()) g.Tree.UpdateView(g, i) }) } func UnMarshalJSON(in io.Reader) (interface{}, error) { b, err := ioutil.ReadAll(in) if err != nil { log.Println(err) return nil, err } if len(b) == 0 { log.Println(err) return nil, ErrEmptyJSON } var i interface{} if err := json.Unmarshal(b, &i); err != nil { log.Println(err) return nil, err } return i, nil }