diff options
Diffstat (limited to 'users/Profpatsch/struct-edit')
-rw-r--r-- | users/Profpatsch/struct-edit/default.nix | 13 | ||||
-rw-r--r-- | users/Profpatsch/struct-edit/main.go | 431 |
2 files changed, 444 insertions, 0 deletions
diff --git a/users/Profpatsch/struct-edit/default.nix b/users/Profpatsch/struct-edit/default.nix new file mode 100644 index 000000000000..970cdd4d028b --- /dev/null +++ b/users/Profpatsch/struct-edit/default.nix @@ -0,0 +1,13 @@ +{ depot, ... }: +depot.nix.buildGo.program { + name = "struct-edit"; + srcs = [ + ./main.go + ]; + deps = [ + depot.third_party.gopkgs."github.com".charmbracelet.bubbletea + depot.third_party.gopkgs."github.com".charmbracelet.lipgloss + depot.third_party.gopkgs."github.com".muesli.termenv + depot.third_party.gopkgs."github.com".mattn.go-isatty + ]; +} diff --git a/users/Profpatsch/struct-edit/main.go b/users/Profpatsch/struct-edit/main.go new file mode 100644 index 000000000000..7e43074266d9 --- /dev/null +++ b/users/Profpatsch/struct-edit/main.go @@ -0,0 +1,431 @@ +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. +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) + } +} |