package main import ( json "encoding/json" "fmt" "log" "os" "strings" "sort" tea "github.com/charmbracelet/bubbletea" lipgloss "github.com/charmbracelet/lipgloss" // termenv "github.com/muesli/termenv" // isatty "github.com/mattn/go-isatty" ) // Keeps the full data structure and a path that indexes our current position into it. // selectedIndex is the currently selected item. (TODO: save per level) type model struct { path []index selectedIndex index data val } // an index into a value, uint for lists and string for maps. // nil for any scalar value. // TODO: use an actual interface for these type index interface{} /// recursive value that we can represent. type val struct { // the “type” of value; see tag const belove tag tag // documentation (TODO) doc string // the actual value; // determined by the tag // tagString -> string // tagFloat -> float64 // tagList -> []val // tagMap -> map[string]val val interface{} } type tag string const ( tagString tag = "string" tagFloat tag = "float" tagList tag = "list" tagMap tag = "map" ) // print a value, flat func (v val) Render() string { s := "" switch v.tag { case tagString: s += v.val.(string) case tagFloat: s += fmt.Sprint(v.val.(float64)) case tagList: s += "[ " vs := []string{} for _, enum := range v.enumerate() { vs = append(vs, enum.v.Render()) } s += strings.Join(vs, ", ") s += " ]" case tagMap: s += "{ " vs := []string{} for _, enum := range v.enumerate() { vs = append(vs, fmt.Sprintf("%s: %s", enum.i.(string), enum.v.Render())) } s += strings.Join(vs, ", ") s += " }" default: s += fmt.Sprintf("", v) } return s } // render an index, depending on the type func renderIndex(i index) (s string) { switch i := i.(type) { case nil: s = "" // list index case uint: s = "*" // map index case string: s = i + ":" } return } // take an arbitrary (within restrictions) go value and construct a val from it func makeVal(i interface{}) val { var v val switch i := i.(type) { case string: v = val{ tag: tagString, doc: "", val: i, } case float64: v = val{ tag: tagFloat, doc: "", val: i, } case []interface{}: ls := []val{} for _, i := range i { ls = append(ls, makeVal(i)) } v = val{ tag: tagList, doc: "", val: ls, } case map[string]interface{}: ls := map[string]val{} for k, i := range i { ls[k] = makeVal(i) } v = val{ tag: tagMap, doc: "", val: ls, } default: log.Fatalf("makeVal: cannot read json of type %T", i) } return v } // return an index that points at the first entry in val func (v val) pos1() index { return v.enumerate()[0].i } type enumerate struct { i index v val } // enumerate gives us a stable ordering of elements in this val. // for scalars it’s just a nil index & the val itself. // Guaranteed to always return at least one element. func (v val) enumerate() (e []enumerate) { switch v.tag { case tagString: fallthrough case tagFloat: e = []enumerate{enumerate{i: index(nil), v: v}} case tagList: for i, v := range v.val.([]val) { e = append(e, enumerate{i: index(uint(i)), v: v}) } case tagMap: // map sorting order is not stable (actually randomized thank jabber) // so let’s sort them keys := []string{} m := v.val.(map[string]val) for k, _ := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { e = append(e, enumerate{i: index(k), v: m[k]}) } default: log.Fatalf("unknown val tag %s, %v", v.tag, v) } return } func (m model) PathString() string { s := "/ " var is []string for _, v := range m.path { is = append(is, fmt.Sprintf("%v", v)) } s += strings.Join(is, " / ") return s } // walk the given path down in data, to get the value at that point. // Assumes that all path indexes are valid indexes into data. func walk(data val, path []index) (val, bool, error) { atPath := func(index int) string { return fmt.Sprintf("at path %v", path[:index+1]) } errf := func(ty string, val interface{}, index int) error { return fmt.Errorf("walk: can’t walk into %s %v %s", ty, val, atPath(index)) } for i, p := range path { switch data.tag { case tagString: return data, true, nil case tagFloat: return data, true, nil case tagList: switch p := p.(type) { case uint: list := data.val.([]val) if int(p) >= len(list) || p < 0 { return data, false, fmt.Errorf("index out of bounds %s", atPath(i)) } data = list[p] default: return data, false, fmt.Errorf("not a list index %s", atPath(i)) } case tagMap: switch p := p.(type) { case string: m := data.val.(map[string]val) var ok bool if data, ok = m[p]; !ok { return data, false, fmt.Errorf("index %s not in map %s", p, atPath(i)) } default: return data, false, fmt.Errorf("not a map index %v %s", p, atPath(i)) } default: return data, false, errf(string(data.tag), data.val, i) } } return data, false, nil } // descend into the selected index. Assumes that the index is valid. // Will not descend into scalars. func (m model) descend() (model, error) { newPath := append(m.path, m.selectedIndex) lower, bounce, err := walk(m.data, newPath) if err != nil { return m, err } // only descend if we *can* if !bounce { m.path = newPath m.selectedIndex = lower.pos1() } return m, nil } // ascend to one level up. stops at the root. func (m model) ascend() (model, error) { if len(m.path) > 0 { m.path = m.path[:len(m.path)-1] upper, _, err := walk(m.data, m.path) m.selectedIndex = upper.pos1() return m, err } return m, nil } /// go to the next item, or wraparound func (min model) next() (m model, err error) { m = min var this val this, _, err = walk(m.data, m.path) if err != nil { return } enumL := this.enumerate() setNext := false for _, enum := range enumL { if setNext { m.selectedIndex = enum.i setNext = false break } if enum.i == m.selectedIndex { setNext = true } } // wraparound if setNext { m.selectedIndex = enumL[0].i } return } /// go to the previous item, or wraparound func (min model) prev() (m model, err error) { m = min var this val this, _, err = walk(m.data, m.path) if err != nil { return } enumL := this.enumerate() // last element, wraparound prevIndex := enumL[len(enumL)-1].i for _, enum := range enumL { if enum.i == m.selectedIndex { m.selectedIndex = prevIndex break } prevIndex = enum.i } return } /// bubbletea implementations func (m model) Init() tea.Cmd { return nil } func initialModel(v interface{}) model { val := makeVal(v) return model{ path: []index{}, data: val, selectedIndex: val.pos1(), } } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var err error switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit case "up": m, err = m.prev() case "down": m, err = m.next() case "right": m, err = m.descend() case "left": m, err = m.ascend() // case "enter": // _, ok := m.selected[m.cursor] // if ok { // delete(m.selected, m.cursor) // } else { // m.selected[m.cursor] = struct{}{} // } } } if err != nil { log.Fatal(err) } return m, nil } var pathColor = lipgloss.NewStyle(). // light blue Foreground(lipgloss.Color("12")) var selectedColor = lipgloss.NewStyle(). Bold(true) func (m model) View() string { s := pathColor.Render(m.PathString()) cur, _, err := walk(m.data, m.path) if err != nil { log.Fatal(err) } s += "\n" for _, enum := range cur.enumerate() { is := renderIndex(enum.i) if is != "" { s += is + " " } if enum.i == m.selectedIndex { s += selectedColor.Render(enum.v.Render()) } else { s += enum.v.Render() } s += "\n" } // s += fmt.Sprintf("%v\n", m) // s += fmt.Sprintf("%v\n", cur) return s } func main() { var input interface{} err := json.NewDecoder(os.Stdin).Decode(&input) if err != nil { log.Fatal("json from stdin: ", err) } p := tea.NewProgram(initialModel(input)) if err := p.Start(); err != nil { log.Fatal("bubbletea TUI error: ", err) } }