tvl-depot/users/Profpatsch/struct-edit/main.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

432 lines
8.7 KiB
Go
Raw Normal View History

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 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)
}
}