The code in //users/wpcarro/tools/monzo_ynab/ynab/client.go was not valid Go and has been commented out. Change-Id: Icb4003607f30294dcbf60132eb7722702c7f0d84 Reviewed-on: https://cl.tvl.fyi/c/depot/+/4400 Tested-by: BuildkiteCI Reviewed-by: wpcarro <wpcarro@gmail.com> Reviewed-by: Profpatsch <mail@profpatsch.de>
		
			
				
	
	
		
			431 lines
		
	
	
	
		
			8.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			431 lines
		
	
	
	
		
			8.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | ||
| 
 | ||
| import (
 | ||
| 	json "encoding/json"
 | ||
| 	"fmt"
 | ||
| 	"log"
 | ||
| 	"os"
 | ||
| 	"sort"
 | ||
| 	"strings"
 | ||
| 
 | ||
| 	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.
 | ||
| type model struct {
 | ||
| 	path []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
 | ||
| 	// last known position of our cursor
 | ||
| 	last_index index
 | ||
| 	// documentation (TODO)
 | ||
| 	doc string
 | ||
| 	// the actual value;
 | ||
| 	// the actual structure is behind a pointer so we can replace the struct.
 | ||
| 	// 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("<unknown: %v>", 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,
 | ||
| 			last_index: index(nil),
 | ||
| 			doc:        "",
 | ||
| 			val:        &i,
 | ||
| 		}
 | ||
| 	case float64:
 | ||
| 		v = val{
 | ||
| 			tag:        tagFloat,
 | ||
| 			last_index: index(nil),
 | ||
| 			doc:        "",
 | ||
| 			val:        &i,
 | ||
| 		}
 | ||
| 	case []interface{}:
 | ||
| 		ls := []val{}
 | ||
| 		for _, i := range i {
 | ||
| 			ls = append(ls, makeVal(i))
 | ||
| 		}
 | ||
| 		v = val{
 | ||
| 			tag:        tagList,
 | ||
| 			last_index: pos1Inner(tagList, &ls),
 | ||
| 			doc:        "",
 | ||
| 			val:        &ls,
 | ||
| 		}
 | ||
| 	case map[string]interface{}:
 | ||
| 		ls := map[string]val{}
 | ||
| 		for k, i := range i {
 | ||
| 			ls[k] = makeVal(i)
 | ||
| 		}
 | ||
| 		v = val{
 | ||
| 			tag:        tagMap,
 | ||
| 			last_index: pos1Inner(tagMap, &ls),
 | ||
| 			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
 | ||
| }
 | ||
| 
 | ||
| func pos1Inner(tag tag, v interface{}) index {
 | ||
| 	return enumerateInner(tag, v)[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) {
 | ||
| 	e = enumerateInner(v.tag, v.val)
 | ||
| 	if e == nil {
 | ||
| 		e = append(e, enumerate{
 | ||
| 			i: nil,
 | ||
| 			v: v,
 | ||
| 		})
 | ||
| 	}
 | ||
| 	return
 | ||
| }
 | ||
| 
 | ||
| // like enumerate, but returns an empty slice for scalars without inner vals.
 | ||
| func enumerateInner(tag tag, v interface{}) (e []enumerate) {
 | ||
| 	switch tag {
 | ||
| 	case tagString:
 | ||
| 		fallthrough
 | ||
| 	case tagFloat:
 | ||
| 		e = nil
 | ||
| 	case tagList:
 | ||
| 		for i, v := range *v.(*[]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.(*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", 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.
 | ||
| // Returns a pointer to the value at point, in order to be able to change it.
 | ||
| func walk(data *val, path []index) (*val, bool, error) {
 | ||
| 	res := data
 | ||
| 	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 res.tag {
 | ||
| 		case tagString:
 | ||
| 			return nil, true, nil
 | ||
| 		case tagFloat:
 | ||
| 			return nil, true, nil
 | ||
| 		case tagList:
 | ||
| 			switch p := p.(type) {
 | ||
| 			case uint:
 | ||
| 				list := *res.val.(*[]val)
 | ||
| 				if int(p) >= len(list) || p < 0 {
 | ||
| 					return nil, false, fmt.Errorf("index out of bounds %s", atPath(i))
 | ||
| 				}
 | ||
| 				res = &list[p]
 | ||
| 			default:
 | ||
| 				return nil, false, fmt.Errorf("not a list index %s", atPath(i))
 | ||
| 			}
 | ||
| 		case tagMap:
 | ||
| 			switch p := p.(type) {
 | ||
| 			case string:
 | ||
| 				m := *res.val.(*map[string]val)
 | ||
| 				if a, ok := m[p]; ok {
 | ||
| 					res = &a
 | ||
| 				} else {
 | ||
| 					return nil, false, fmt.Errorf("index %s not in map %s", p, atPath(i))
 | ||
| 				}
 | ||
| 			default:
 | ||
| 				return nil, false, fmt.Errorf("not a map index %v %s", p, atPath(i))
 | ||
| 			}
 | ||
| 
 | ||
| 		default:
 | ||
| 			return nil, false, errf(string(res.tag), res.val, i)
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return res, false, nil
 | ||
| }
 | ||
| 
 | ||
| // descend into the selected index. Assumes that the index is valid.
 | ||
| // Will not descend into scalars.
 | ||
| func (m model) descend() (model, error) {
 | ||
| 	// TODO: two walks?!
 | ||
| 	this, _, err := walk(&m.data, m.path)
 | ||
| 	if err != nil {
 | ||
| 		return m, err
 | ||
| 	}
 | ||
| 	newPath := append(m.path, this.last_index)
 | ||
| 	_, bounce, err := walk(&m.data, newPath)
 | ||
| 	if err != nil {
 | ||
| 		return m, err
 | ||
| 	}
 | ||
| 	// only descend if we *can*
 | ||
| 	if !bounce {
 | ||
| 		m.path = newPath
 | ||
| 	}
 | ||
| 	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]
 | ||
| 		_, _, err := walk(&m.data, m.path)
 | ||
| 		return m, err
 | ||
| 	}
 | ||
| 	return m, nil
 | ||
| }
 | ||
| 
 | ||
| /// go to the next item, or wraparound
 | ||
| func (min model) next() (m model, err error) {
 | ||
| 	m = min
 | ||
| 	this, _, err := walk(&m.data, m.path)
 | ||
| 	if err != nil {
 | ||
| 		return
 | ||
| 	}
 | ||
| 	enumL := this.enumerate()
 | ||
| 	setNext := false
 | ||
| 	for _, enum := range enumL {
 | ||
| 		if setNext {
 | ||
| 			this.last_index = enum.i
 | ||
| 			setNext = false
 | ||
| 			break
 | ||
| 		}
 | ||
| 		if enum.i == this.last_index {
 | ||
| 			setNext = true
 | ||
| 		}
 | ||
| 	}
 | ||
| 	// wraparound
 | ||
| 	if setNext {
 | ||
| 		this.last_index = enumL[0].i
 | ||
| 	}
 | ||
| 	return
 | ||
| }
 | ||
| 
 | ||
| /// go to the previous item, or wraparound
 | ||
| func (min model) prev() (m model, err error) {
 | ||
| 	m = min
 | ||
| 	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 == this.last_index {
 | ||
| 			this.last_index = 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,
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| 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 += cur.doc + "\n"
 | ||
| 	s += "\n"
 | ||
| 	for _, enum := range cur.enumerate() {
 | ||
| 		is := renderIndex(enum.i)
 | ||
| 		if is != "" {
 | ||
| 			s += is + " "
 | ||
| 		}
 | ||
| 		if enum.i == cur.last_index {
 | ||
| 			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)
 | ||
| 	}
 | ||
| }
 |