tvl-depot/users/Profpatsch/struct-edit/main.go
Profpatsch 5156da542b feat(users/Profpatsch/struct-edit): per-level position
The user expects the editor to remember the positions of fields they
navigated from to a new level, so when they return they get put in the
same spot.

We push the index from one field into every level of the value.

Unfortunately this introduces pointers and all the woes they bring.

Change-Id: I889c28b71fd7082b765e1d6874faeb1b36dade60
Reviewed-on: https://cl.tvl.fyi/c/depot/+/2866
Tested-by: BuildkiteCI
Reviewed-by: Profpatsch <mail@profpatsch.de>
2021-04-23 18:30:06 +00:00

431 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 its 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 lets 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: cant 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)
}
}