From 385b6ea67ce15d3456290d51f12233c33eaeada0 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 19 Feb 2026 11:50:25 -0600 Subject: [PATCH] Work --- app/app.go | 7 +- app/screen_home.go | 15 +- widgets/json_content.go | 348 +++++++++++++++++++++++++++++----------- 3 files changed, 276 insertions(+), 94 deletions(-) diff --git a/app/app.go b/app/app.go index a3cdcdb..8fef8c4 100644 --- a/app/app.go +++ b/app/app.go @@ -149,7 +149,12 @@ func (a *App) PostEvent(ev tcell.Event) error { func (a *App) Sync() { a.tScreen.Sync() } func (a *App) ClearScreen() { a.tScreen.Clear() } func (a *App) Exit() { a.running = false } -func (a *App) Cleanup() { a.tScreen.Fini() } +func (a *App) Cleanup() { + a.running = false + a.tScreen.Sync() + a.tScreen.Clear() + a.tScreen.Fini() +} func (a *App) GetRepo() *data.Repo { return a.repo } diff --git a/app/screen_home.go b/app/screen_home.go index ab0691e..9c3e2c6 100644 --- a/app/screen_home.go +++ b/app/screen_home.go @@ -118,6 +118,7 @@ func (s *ScreenHome) Init(a *App) { s.pdsListing.SetLogger(s.Log) s.jsonContent = wd.NewJsonContent("jsoncontent", s.style) + s.jsonContent.SetLogger(s.Log) km := s.jsonContent.GetKeyMap() km.Add( w.NewKey(w.BuildEK(tcell.KeyEnter), func(ev *tcell.EventKey) bool { @@ -127,11 +128,21 @@ func (s *ScreenHome) Init(a *App) { s.Log("Error initializing clipboard: %s", err.Error()) return true } - clipboard.Write(clipboard.FmtText, []byte(s.jsonContent.GetSelectedValue())) + v, err := s.jsonContent.GetSelectedValue() + if err != nil { + s.Log("Error getting selected value: %s", err.Error()) + return true + } + clipboard.Write(clipboard.FmtText, []byte(v)) return true }), w.NewKey(w.BuildEKr('O'), func(ev *tcell.EventKey) bool { - url := fmt.Sprintf("https://%s/xrpc/com.atproto.sync.getBlob?did=did:plc:pqwuemo2ic5tqmpwrajb2phi&cid=%s", s.activePds.AtId.String(), s.jsonContent.GetSelectedValue()) + v, err := s.jsonContent.GetSelectedValue() + if err != nil { + s.Log("Error getting selected value: %s", err.Error()) + return true + } + url := fmt.Sprintf("https://%s/xrpc/com.atproto.sync.getBlob?did=did:plc:pqwuemo2ic5tqmpwrajb2phi&cid=%s", s.activePds.AtId.String(), v) open.Run(url) return true }), diff --git a/widgets/json_content.go b/widgets/json_content.go index d77fdd7..3039f5c 100644 --- a/widgets/json_content.go +++ b/widgets/json_content.go @@ -23,6 +23,7 @@ package widgets import ( "encoding/json" + "errors" "fmt" "strings" @@ -47,6 +48,9 @@ type JsonContent struct { editable bool value any + jsonObj map[string]json.RawMessage + jsonArr json.RawMessage + isObj bool activeNode []string @@ -55,6 +59,8 @@ type JsonContent struct { editKey, editVal bool editValType int + + logger func(string, ...any) } const ( @@ -63,6 +69,7 @@ const ( JsonTpNumber JsonTpObject JsonTpArray + JsonTpErr ) var _ wd.Widget = (*JsonContent)(nil) @@ -173,67 +180,24 @@ func (w *JsonContent) Draw(screen tcell.Screen) { if len(txt) > w.w-brdSz && w.w-brdSz >= 0 { txt = txt[:(w.w - brdSz)] } - if w.editable { - w.drawEditableLine(screen, i, w.x, y, txt, stl, dim) + if w.editable && i == w.cursor { + pth, err := w.GetSelectedPath() + if err != nil { + wh.DrawText(x, y, fmt.Sprintf("%s", err.Error()), stl, screen) + } else { + key, val, err := w.getItemKeyVal(w.cursor) + if err != nil { + wh.DrawText(x, y, fmt.Sprintf("%s: %s", pth, err.Error()), stl, screen) + } else { + wh.DrawText(x, y, fmt.Sprintf("%s: %s -> %s", pth, key, val), stl, screen) + } + } } else { - w.drawReadOnlyLine(screen, i, w.x, y, txt, stl, dim) + wh.DrawText(x, y, txt, stl.Dim(dim).Bold(!dim), screen) } y++ } } -func (w *JsonContent) drawReadOnlyLine(screen tcell.Screen, idx, x, y int, txt string, stl tcell.Style, dim bool) { - wh.DrawText(x, y, txt, stl.Dim(dim).Bold(!dim), screen) -} -func (w *JsonContent) drawEditableLine(screen tcell.Screen, idx, x, y int, txt string, stl tcell.Style, dim bool) { - parseQuoted := func(txt string) (string, string) { - var v string - var slash bool - for i := range txt { - if txt[i] == '\\' { - slash = true - } else if slash { - slash = false - } else if txt[i] == '"' { - return v, txt[i+1:] - } - v = fmt.Sprintf("%s%c", v, txt[i]) - } - // We didn't hit an ending quote - return v, "" - } - var key, val, wrk string - // The key should be a quoted value - for i := range txt { - if txt[i] == '"' && len(txt) > i { - key, wrk = parseQuoted(txt[i:]) - break - } - } - txt = strings.Trim(wrk, ": ") - if len(txt) > 0 { - switch txt[0] { - case '"': - val, wrk = parseQuoted(txt[1:]) - w.editValType = JsonTpString - case '{': - w.editValType = JsonTpObject - case '[': - w.editValType = JsonTpArray - default: - if txt == "true" || txt == "false" { - // Editing a boolean value - w.editValType = JsonTpBool - } else { - // Must be a number, right? - w.editValType = JsonTpNumber - } - } - } - - keyStr := fmt.Sprintf("%s: ", key) - wh.DrawText(w.x, y, keyStr, stl.Dim(dim).Bold(!dim), screen) - wh.DrawText(w.x, y+len(keyStr), val, stl.Dim(dim).Bold(!dim), screen) -} func (w *JsonContent) SetStyle(s tcell.Style) { w.style = s } func (w *JsonContent) Active() bool { return w.active } @@ -268,8 +232,28 @@ func (w *JsonContent) SetValue(v any) error { } w.valueString = string(bts) w.contents = strings.Split(w.valueString, "\n") - return nil + if bts[0] == '{' { + err = json.Unmarshal(bts, &w.jsonObj) + w.isObj = true + } else if bts[0] == '[' { + err = json.Unmarshal(bts, &w.jsonArr) + w.isObj = false + } else { + // Item is just a flat string/number/boolean + w.cursor = 0 + } + if !w.isLineSelectable(w.cursor) { + idx := w.findPrevSelectableLine(w.cursor) + if idx == -1 { + idx = w.findNextSelectableLine(w.cursor) + } + if idx >= 0 { + w.cursor = idx + } + } + return err } + func (w *JsonContent) SetBorder(brd []rune) { if len(brd) == 0 { w.border = wh.BRD_SIMPLE @@ -280,57 +264,239 @@ func (w *JsonContent) SetBorder(brd []rune) { func (w *JsonContent) SetEditable(v bool) { w.editable = v } -/* - func (w *JsonContent) SetJsonContent(txt string) { - w.text = txt - if strings.Contains(w.text, "\n") { - w.contents = strings.Split(w.text, "\n") - } else { - w.contents = []string{w.text} - } - } - -func (w *JsonContent) GetJsonContent() string { return w.text } -func (w *JsonContent) GetContents() []string { return w.contents } -*/ - func (w *JsonContent) SetVimMode(b bool) { w.vimMode = b } func (w *JsonContent) MoveUp() bool { - w.cursor-- - if w.cursor < 0 { - w.cursor = 0 + idx := w.findPrevSelectableLine(w.cursor) + if idx < 0 { + return false } + w.cursor = idx return true } func (w *JsonContent) MoveDown() bool { - w.cursor++ - if w.cursor >= len(w.contents) { - w.cursor = len(w.contents) - 1 + idx := w.findNextSelectableLine(w.cursor) + if idx < 0 { + return false } + w.cursor = idx return true } func (w *JsonContent) PageUp() bool { - w.cursor -= w.h - if w.cursor < 0 { - w.cursor = 0 + idx := w.findPrevSelectableLine(w.cursor - w.h) + if idx < 0 { + return false } + w.cursor = idx return true } func (w *JsonContent) PageDn() bool { - w.cursor += w.h - if w.cursor > len(w.contents)-1 { - w.cursor = len(w.contents) - 1 + idx := w.findNextSelectableLine(w.cursor + w.h) + if idx < 0 { + return false } + w.cursor = idx return true } -func (w *JsonContent) GetSelectedValue() string { - if w.cursor >= len(w.contents) { - w.cursor = len(w.contents) - 1 +func (w *JsonContent) GetSelectedPath() ([]string, error) { return w.getItemPath(w.cursor) } +func (w *JsonContent) GetSelectedKey() (string, error) { return w.getItemKey(w.cursor) } +func (w *JsonContent) GetSelectedValue() (string, error) { return w.getItemVal(w.cursor) } +func (w *JsonContent) getSelectedItemDepth() int { return w.getItemDepth(w.cursor) } + +func (w *JsonContent) getItemPath(line int) ([]string, error) { + if line < 0 || line >= len(w.contents) { + return []string{}, errors.New("invalid line number") } - var ret string - ret = w.contents[w.cursor] - ret = ret[strings.Index(ret, "\": \"")+3:] - ret = ret[1 : len(ret)-1] - return ret + depth := w.getItemDepth(line) + if depth == 1 { + key, err := w.getItemKey(line) + if err != nil { + return []string{}, err + } + return []string{key}, nil + } + pth, err := w.getItemPath(w.findItemParentLine(line)) + if err != nil { + return []string{}, err + } + key, err := w.getItemKey(line) + if err != nil { + return []string{}, err + } + return append(pth, key), nil } + +func (w *JsonContent) getItemKeyVal(line int) (string, string, error) { + if line < 0 || line >= len(w.contents) { + return "", "", errors.New("invalid line number") + } + text := w.contents[line] + key, rest := w.getNextQuotedString(text) + if key == "" { + return "", "", errors.New("error finding quoted string") + } else if rest == "" { + // We have a 'key' value, but no 'rest' so this is likely an item in array + return "", key, nil + } + rest = strings.Trim(rest, ":{[}], ") + val, rest := w.getNextQuotedString(rest) + if val != "" { + return key, val, nil + } + return key, rest, nil +} + +func (w *JsonContent) getItemKey(line int) (string, error) { + if line < 0 || line >= len(w.contents) { + return "", errors.New("invalid line number") + } + + key, _ := w.getNextQuotedString(w.contents[line]) + if key == "" { + return "", errors.New("error finding quoted string") + } + return key, nil +} + +func (w *JsonContent) getItemVal(line int) (string, error) { + if line < 0 || line >= len(w.contents) { + return "", errors.New("invalid line number") + } + var key, val, rest string + key, rest = w.getNextQuotedString(w.contents[line]) + if len(rest) > 0 { + val, rest = w.getNextQuotedString(rest) + } + if len(val) > 0 { + return val, nil + } + if len(key) > 0 { + return key, nil + } + return "", errors.New("error finding value") +} + +/* +func (w *JsonContent) getItemValType(line int) int { + if line < 0 || line >= len(w.contents) { + return JsonTypeErr + } + + text := w.contents[line] + key, rest := w.getNextQuotedString(text) + rest = strings.Trim(rest, ", ") + if rest == "" { + if key != "" { + // Looks like a string value in an array + return JsonTypeString + } else rest == "true" || rest == "false" { + return JsonTypeBool + } + // see if we can parse this as a number + num, err := strconv.Atoi(rest) + if err == nil { + return JsonTypeNumber + } + return JsonType + } + if key == "" { + return "", "", errors.New("error finding quoted string") + } else if rest == "" { + // We have a 'key' value, but no 'rest' so this is likely an item in array + return "", key, nil + } + rest = strings.Trim(rest, ":, ") + if rest[0] == '{' { + + } + val, rest := w.getNextQuotedString(rest) + if val != "" { + return key, val, nil + } + // Val isn't a quoted string, + return key, rest, nil +} +*/ + +func (w *JsonContent) getItemDepth(line int) int { + if line < 0 || line >= len(w.contents) { + return 0 + } + txt := w.contents[line] + // Find the depth, we use two spaces to indent + for i := range txt { + if txt[i] != ' ' { + return i / 2 + } + } + return 0 +} + +func (w *JsonContent) findItemParentLine(line int) int { + depth := w.getItemDepth(line) + if depth <= 1 { + return -1 + } + for i := w.cursor; i >= 0; i-- { + if w.getItemDepth(i) < depth { + return i + } + } + return -1 +} + +func (w *JsonContent) getItemParentKey(line int) (string, error) { + return w.getItemKey(w.findItemParentLine(line)) +} +func (w *JsonContent) getItemParentVal(line int) (string, error) { + return w.getItemVal(w.findItemParentLine(line)) +} +func (w *JsonContent) getNextQuotedString(line string) (string, string) { + var inQ, slash bool + var ret string + for i := range line { + if inQ { + if line[i] == '"' && !slash { + return ret, line[i+1:] + } else if slash { + slash = false + } else if line[i] == '\\' { + slash = true + } + ret = fmt.Sprintf("%s%c", ret, line[i]) + } + if line[i] == '"' { + inQ = true + } + } + // if we're here, we didn't find a quoted string + return "", line +} + +func (w *JsonContent) isLineSelectable(line int) bool { + if line < 0 || line >= len(w.contents) { + return false + } + return strings.Contains(w.contents[line], "\"") +} + +func (w *JsonContent) findPrevSelectableLine(from int) int { + for i := from - 1; i >= 0; i-- { + if w.isLineSelectable(i) { + return i + } + } + return -1 +} + +func (w *JsonContent) findNextSelectableLine(from int) int { + for i := from + 1; i < len(w.contents); i++ { + if w.isLineSelectable(i) { + return i + } + } + return -1 +} + +func (w *JsonContent) SetLogger(l func(string, ...any)) { w.logger = l } +func (w *JsonContent) Log(txt string, args ...any) { w.logger(txt, args...) }