diff --git a/keymap.go b/keymap.go index effca80..4057399 100644 --- a/keymap.go +++ b/keymap.go @@ -3,24 +3,28 @@ package main import "github.com/charmbracelet/bubbles/key" type KeyMap struct { - Quit key.Binding - Help key.Binding - PageDown key.Binding - PageUp key.Binding - HalfPageUp key.Binding - HalfPageDown key.Binding - Down key.Binding - Up key.Binding - Expand key.Binding - Collapse key.Binding - GotoTop key.Binding - GotoBottom key.Binding - ToggleWrap key.Binding - ExpandAll key.Binding - CollapseAll key.Binding - Search key.Binding - Next key.Binding - Prev key.Binding + Quit key.Binding + Help key.Binding + PageDown key.Binding + PageUp key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding + Down key.Binding + Up key.Binding + Expand key.Binding + Collapse key.Binding + NextSibling key.Binding + PrevSibling key.Binding + ExpandRecursively key.Binding + CollapseRecursively key.Binding + GotoTop key.Binding + GotoBottom key.Binding + ToggleWrap key.Binding + ExpandAll key.Binding + CollapseAll key.Binding + Search key.Binding + Next key.Binding + Prev key.Binding } func DefaultKeyMap() KeyMap { @@ -65,6 +69,22 @@ func DefaultKeyMap() KeyMap { key.WithKeys("left", "h"), key.WithHelp("", "collapse"), ), + NextSibling: key.NewBinding( + key.WithKeys("J"), + key.WithHelp("", "next sibling"), + ), + PrevSibling: key.NewBinding( + key.WithKeys("K"), + key.WithHelp("", "previous sibling"), + ), + ExpandRecursively: key.NewBinding( + key.WithKeys("L"), + key.WithHelp("", "expand recursively"), + ), + CollapseRecursively: key.NewBinding( + key.WithKeys("H"), + key.WithHelp("", "collapse recursively"), + ), GotoTop: key.NewBinding( key.WithKeys("g"), key.WithHelp("", "goto top"), diff --git a/main.go b/main.go index 78589e7..5db0487 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,7 @@ var colors = struct { func main() { filePath := "" + args := []string{} var dec *json.Decoder if term.IsTerminal(int(os.Stdin.Fd())) { if len(os.Args) >= 2 { @@ -48,9 +49,11 @@ func main() { panic(err) } dec = json.NewDecoder(f) + args = os.Args[2:] } } else { dec = json.NewDecoder(os.Stdin) + args = os.Args[1:] } dec.UseNumber() jsonObject, err := parse(dec) @@ -58,24 +61,37 @@ func main() { panic(err) } + if len(args) > 0 { + if args[0] == "--print-code" { + fmt.Print(generateCode(args[1:])) + return + } + reduce(jsonObject, args) + return + } + expand := map[string]bool{ "": true, } - if array, ok := jsonObject.(array); ok { for i := range array { expand[accessor("", i)] = true } } - parents := map[string]string{} + children := map[string][]string{} + canBeExpanded := map[string]bool{} dfs(jsonObject, func(it iterator) { parents[it.path] = it.parent - }) + children[it.parent] = append(children[it.parent], it.path) + switch it.object.(type) { + case *dict, array: + canBeExpanded[it.path] = true + } + }) input := textinput.New() input.Prompt = "" - m := &model{ fileName: path.Base(filePath), json: jsonObject, @@ -84,7 +100,9 @@ func main() { mouseWheelDelta: 3, keyMap: DefaultKeyMap(), expandedPaths: expand, + canBeExpanded: canBeExpanded, parents: parents, + children: children, wrap: true, searchInput: input, } @@ -112,12 +130,13 @@ type model struct { keyMap KeyMap showHelp bool - expandedPaths map[string]bool // a set with expanded paths - canBeExpanded map[string]bool // a set for path => can be expanded (i.e. dict or array) - pathToLineNumber *dict // dict with path => line number - lineNumberToPath map[int]string // map of line number => path - parents map[string]string // map of subpath => parent path - cursor int // cursor in range of m.pathToLineNumber.keys slice + expandedPaths map[string]bool // a set with expanded paths + canBeExpanded map[string]bool // a set for path => can be expanded (i.e. dict or array) + pathToLineNumber *dict // dict with path => line number + lineNumberToPath map[int]string // map of line number => path + parents map[string]string // map of subpath => parent path + children map[string][]string // map of path => child paths + cursor int // cursor in range of m.pathToLineNumber.keys slice showCursor bool wrap bool @@ -234,6 +253,18 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.SetOffset(at) } + case key.Matches(msg, m.keyMap.NextSibling): + m.showCursor = true + // TODO: write code for collecting siblings, + // and write code to getting sibling and jumping to it. + m.render() + at := m.cursorLineNumber() + if m.offset <= at { // cursor is lower + m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0 + } else { + m.SetOffset(at) + } + case key.Matches(msg, m.keyMap.Up): m.showCursor = true if m.cursor > 0 { @@ -255,14 +286,27 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.expandedPaths[m.cursorPath()] = true m.render() - case key.Matches(msg, m.keyMap.Collapse): + case key.Matches(msg, m.keyMap.ExpandRecursively): + m.showCursor = true + m.expandRecursively(m.cursorPath()) + m.render() + + case key.Matches(msg, m.keyMap.Collapse, m.keyMap.CollapseRecursively): m.showCursor = true if m.expandedPaths[m.cursorPath()] { - m.expandedPaths[m.cursorPath()] = false + if key.Matches(msg, m.keyMap.CollapseRecursively) { + m.collapseRecursively(m.cursorPath()) + } else { + m.expandedPaths[m.cursorPath()] = false + } } else { parentPath, ok := m.parents[m.cursorPath()] if ok { - m.expandedPaths[parentPath] = false + if key.Matches(msg, m.keyMap.CollapseRecursively) { + m.collapseRecursively(m.cursorPath()) + } else { + m.expandedPaths[m.cursorPath()] = false + } index, _ := m.pathToLineNumber.index(parentPath) m.cursor = index } @@ -382,7 +426,6 @@ func (m *model) render() { } m.pathToLineNumber = newDict() - m.canBeExpanded = map[string]bool{} m.lineNumberToPath = map[int]string{} m.lines = m.print(m.json, 1, 0, 0, "", false) @@ -407,3 +450,23 @@ func (m *model) cursorLineNumber() int { } return -1 } + +func (m *model) expandRecursively(path string) { + if m.canBeExpanded[path] { + m.expandedPaths[path] = true + for _, childPath := range m.children[path] { + if childPath != "" { + m.expandRecursively(childPath) + } + } + } +} + +func (m *model) collapseRecursively(path string) { + m.expandedPaths[path] = false + for _, childPath := range m.children[path] { + if childPath != "" { + m.collapseRecursively(childPath) + } + } +} diff --git a/print.go b/print.go index 75dd0e5..6be0252 100644 --- a/print.go +++ b/print.go @@ -7,6 +7,68 @@ import ( "strings" ) +func prettyPrint(v interface{}, level int) string { + ident := strings.Repeat(" ", level) + subident := strings.Repeat(" ", level-1) + switch v.(type) { + case nil: + return colors.null.Render("null") + + case bool: + if v.(bool) { + return colors.boolean.Render("true") + } else { + return colors.boolean.Render("false") + } + + case json.Number: + return colors.number.Render(v.(json.Number).String()) + + case string: + return colors.string.Render(fmt.Sprintf("%q", v)) + + case *dict: + keys := v.(*dict).keys + if len(keys) == 0 { + return colors.bracket.Render("{}") + } + output := colors.bracket.Render("{\n") + for i, k := range keys { + key := colors.key.Render(fmt.Sprintf("%q", k)) + value, _ := v.(*dict).get(k) + delim := ": " + line := ident + key + delim + prettyPrint(value, level+1) + if i < len(keys)-1 { + line += ",\n" + } else { + line += "\n" + } + output += line + } + return output + subident + colors.bracket.Render("}") + + case array: + slice := v.(array) + if len(slice) == 0 { + return colors.bracket.Render("[]") + } + output := colors.bracket.Render("[\n") + for i, value := range v.(array) { + line := ident + prettyPrint(value, level+1) + if i < len(slice)-1 { + line += ",\n" + } else { + line += "\n" + } + output += line + } + return output + subident + colors.bracket.Render("]") + + default: + return "unknown type" + } +} + func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path string, dontHighlightCursor bool) []string { ident := strings.Repeat(" ", level) subident := strings.Repeat(" ", level-1) @@ -50,7 +112,6 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri case *dict: m.pathToLineNumber.set(path, lineNumber) - m.canBeExpanded[path] = true m.lineNumberToPath[lineNumber] = path bracketStyle := cursorOr(colors.bracket).Render if len(v.(*dict).keys) == 0 { @@ -102,7 +163,6 @@ func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path stri case array: m.pathToLineNumber.set(path, lineNumber) - m.canBeExpanded[path] = true m.lineNumberToPath[lineNumber] = path bracketStyle := cursorOr(colors.bracket).Render if len(v.(array)) == 0 { diff --git a/search.go b/search.go index 0c63252..988ff6d 100644 --- a/search.go +++ b/search.go @@ -86,16 +86,16 @@ func (m *model) jumpToSearchResult(at int) { m.SetOffset(lineNumber.(int)) m.render() } else { - m.recursiveExpand(desiredPath) + m.expandToPath(desiredPath) m.render() m.jumpToSearchResult(at) } } -func (m *model) recursiveExpand(path string) { +func (m *model) expandToPath(path string) { m.expandedPaths[path] = true if path != "" { - m.recursiveExpand(m.parents[path]) + m.expandToPath(m.parents[path]) } } diff --git a/util.go b/util.go index 2df2cce..0914f5e 100644 --- a/util.go +++ b/util.go @@ -27,12 +27,49 @@ func max(a, b int) int { return b } -func stringify(x interface{}) string { - str, err := json.Marshal(x) - if err != nil { - panic(err) +func stringify(v interface{}) string { + switch v.(type) { + case nil: + return "null" + + case bool: + if v.(bool) { + return "true" + } else { + return "false" + } + + case json.Number: + return v.(json.Number).String() + + case string: + return fmt.Sprintf("%q", v) + + case *dict: + result := "{" + for i, key := range v.(*dict).keys { + line := fmt.Sprintf("%q", key) + ":" + stringify(v.(*dict).values[key]) + if i < len(v.(*dict).keys)-1 { + line += "," + } + result += line + } + return result + "}" + + case array: + result := "[" + for i, value := range v.(array) { + line := stringify(value) + if i < len(v.(array))-1 { + line += "," + } + result += line + } + return result + "]" + + default: + return "unknown type" } - return string(str) } func width(s string) int {