about summary refs log tree commit diff
path: root/users/Profpatsch/struct-edit/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'users/Profpatsch/struct-edit/main.go')
-rw-r--r--users/Profpatsch/struct-edit/main.go431
1 files changed, 431 insertions, 0 deletions
diff --git a/users/Profpatsch/struct-edit/main.go b/users/Profpatsch/struct-edit/main.go
new file mode 100644
index 000000000000..c1a701338534
--- /dev/null
+++ b/users/Profpatsch/struct-edit/main.go
@@ -0,0 +1,431 @@
+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)
+	}
+}