forked from Archives/fx
Reimplement in go
parent
65a1608462
commit
e6042cb698
@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
type dict struct {
|
||||
keys []string
|
||||
indexes map[string]int
|
||||
values map[string]interface{}
|
||||
}
|
||||
|
||||
func newDict() *dict {
|
||||
d := dict{}
|
||||
d.keys = []string{}
|
||||
d.indexes = map[string]int{}
|
||||
d.values = map[string]interface{}{}
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *dict) get(key string) (interface{}, bool) {
|
||||
val, exists := d.values[key]
|
||||
return val, exists
|
||||
}
|
||||
|
||||
func (d *dict) index(key string) (int, bool) {
|
||||
index, exists := d.indexes[key]
|
||||
return index, exists
|
||||
}
|
||||
|
||||
func (d *dict) set(key string, value interface{}) {
|
||||
_, exists := d.values[key]
|
||||
if !exists {
|
||||
d.indexes[key] = len(d.keys)
|
||||
d.keys = append(d.keys, key)
|
||||
}
|
||||
d.values[key] = value
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_dict(t *testing.T) {
|
||||
d := newDict()
|
||||
d.set("number", 3)
|
||||
v, _ := d.get("number")
|
||||
if v.(int) != 3 {
|
||||
t.Error("Set number")
|
||||
}
|
||||
i, _ := d.index("number")
|
||||
if i != 0 {
|
||||
t.Error("Index error")
|
||||
}
|
||||
// string
|
||||
d.set("string", "x")
|
||||
v, _ = d.get("string")
|
||||
if v.(string) != "x" {
|
||||
t.Error("Set string")
|
||||
}
|
||||
i, _ = d.index("string")
|
||||
if i != 1 {
|
||||
t.Error("Index error")
|
||||
}
|
||||
// string slice
|
||||
d.set("strings", []string{
|
||||
"t",
|
||||
"u",
|
||||
})
|
||||
v, _ = d.get("strings")
|
||||
if v.([]string)[0] != "t" {
|
||||
t.Error("Set strings first index")
|
||||
}
|
||||
if v.([]string)[1] != "u" {
|
||||
t.Error("Set strings second index")
|
||||
}
|
||||
i, _ = d.index("strings")
|
||||
if i != 2 {
|
||||
t.Error("Index error")
|
||||
}
|
||||
// mixed slice
|
||||
d.set("mixed", []interface{}{
|
||||
1,
|
||||
"1",
|
||||
})
|
||||
v, _ = d.get("mixed")
|
||||
if v.([]interface{})[0].(int) != 1 {
|
||||
t.Error("Set mixed int")
|
||||
}
|
||||
if v.([]interface{})[1].(string) != "1" {
|
||||
t.Error("Set mixed string")
|
||||
}
|
||||
// overriding existing key
|
||||
d.set("number", 4)
|
||||
v, _ = d.get("number")
|
||||
if v.(int) != 4 {
|
||||
t.Error("Override existing key")
|
||||
}
|
||||
// keys
|
||||
expectedKeys := []string{
|
||||
"number",
|
||||
"string",
|
||||
"strings",
|
||||
"mixed",
|
||||
}
|
||||
for i, key := range d.keys {
|
||||
if key != expectedKeys[i] {
|
||||
t.Error("Keys method", key, "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
for i, key := range expectedKeys {
|
||||
if key != expectedKeys[i] {
|
||||
t.Error("Keys method", key, "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
module github.com/antonmedv/fx
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.10.3
|
||||
github.com/charmbracelet/bubbletea v0.20.0
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
|
||||
)
|
@ -0,0 +1,50 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
|
||||
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
|
||||
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
||||
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
|
||||
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
|
||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var helpStyle = lipgloss.NewStyle().PaddingLeft(4).PaddingTop(2).PaddingBottom(2)
|
||||
|
||||
func (m *model) helpView() []string {
|
||||
v := reflect.ValueOf(m.keyMap)
|
||||
fields := reflect.VisibleFields(v.Type())
|
||||
|
||||
keys := make([]string, 0)
|
||||
for i := range fields {
|
||||
k := v.Field(i).Interface().(key.Binding)
|
||||
str := k.Help().Key
|
||||
if width(str) == 0 {
|
||||
str = strings.Join(k.Keys(), ", ")
|
||||
}
|
||||
keys = append(keys, fmt.Sprintf("%v ", str))
|
||||
}
|
||||
|
||||
desc := make([]string, 0)
|
||||
for i := range fields {
|
||||
k := v.Field(i).Interface().(key.Binding)
|
||||
desc = append(desc, fmt.Sprintf("%v", k.Help().Desc))
|
||||
}
|
||||
|
||||
content := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
strings.Join(keys, "\n"),
|
||||
strings.Join(desc, "\n"),
|
||||
)
|
||||
|
||||
return strings.Split(helpStyle.Render(content), "\n")
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
type KeyMap struct {
|
||||
Quit key.Binding
|
||||
Help key.Binding
|
||||
PageDown key.Binding
|
||||
PageUp key.Binding
|
||||
HalfPageUp key.Binding
|
||||
HalfPageDown key.Binding
|
||||
Down key.Binding
|
||||
Up key.Binding
|
||||
Expand key.Binding
|
||||
Collapse key.Binding
|
||||
GotoTop key.Binding
|
||||
GotoBottom key.Binding
|
||||
ToggleWrap key.Binding
|
||||
ExpandAll key.Binding
|
||||
CollapseAll key.Binding
|
||||
Search key.Binding
|
||||
Next key.Binding
|
||||
Prev key.Binding
|
||||
}
|
||||
|
||||
func DefaultKeyMap() KeyMap {
|
||||
return KeyMap{
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q", "ctrl+c", "esc"),
|
||||
key.WithHelp("", "exit program"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("", "show help"),
|
||||
),
|
||||
PageDown: key.NewBinding(
|
||||
key.WithKeys("pgdown", " ", "f"),
|
||||
key.WithHelp("pgdown, space, f", "page down"),
|
||||
),
|
||||
PageUp: key.NewBinding(
|
||||
key.WithKeys("pgup", "b"),
|
||||
key.WithHelp("pgup, b", "page up"),
|
||||
),
|
||||
HalfPageUp: key.NewBinding(
|
||||
key.WithKeys("u", "ctrl+u"),
|
||||
key.WithHelp("", "half page up"),
|
||||
),
|
||||
HalfPageDown: key.NewBinding(
|
||||
key.WithKeys("d", "ctrl+d"),
|
||||
key.WithHelp("", "half page down"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("", "down"),
|
||||
),
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("", "up"),
|
||||
),
|
||||
Expand: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("", "expand"),
|
||||
),
|
||||
Collapse: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("", "collapse"),
|
||||
),
|
||||
GotoTop: key.NewBinding(
|
||||
key.WithKeys("g"),
|
||||
key.WithHelp("", "goto top"),
|
||||
),
|
||||
GotoBottom: key.NewBinding(
|
||||
key.WithKeys("G"),
|
||||
key.WithHelp("", "goto bottom"),
|
||||
),
|
||||
ToggleWrap: key.NewBinding(
|
||||
key.WithKeys("z"),
|
||||
key.WithHelp("", "toggle strings wrap"),
|
||||
),
|
||||
ExpandAll: key.NewBinding(
|
||||
key.WithKeys("e"),
|
||||
key.WithHelp("", "expand all"),
|
||||
),
|
||||
CollapseAll: key.NewBinding(
|
||||
key.WithKeys("E"),
|
||||
key.WithHelp("", "collapse all"),
|
||||
),
|
||||
Search: key.NewBinding(
|
||||
key.WithKeys("/"),
|
||||
key.WithHelp("", "search regexp"),
|
||||
),
|
||||
Next: key.NewBinding(
|
||||
key.WithKeys("n"),
|
||||
key.WithHelp("", "next search result"),
|
||||
),
|
||||
Prev: key.NewBinding(
|
||||
key.WithKeys("N"),
|
||||
key.WithHelp("", "prev search result"),
|
||||
),
|
||||
}
|
||||
}
|
@ -0,0 +1,409 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"golang.org/x/term"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var colors = struct {
|
||||
cursor lipgloss.Style
|
||||
bracket lipgloss.Style
|
||||
key lipgloss.Style
|
||||
null lipgloss.Style
|
||||
boolean lipgloss.Style
|
||||
number lipgloss.Style
|
||||
string lipgloss.Style
|
||||
preview lipgloss.Style
|
||||
statusBar lipgloss.Style
|
||||
search lipgloss.Style
|
||||
}{
|
||||
cursor: lipgloss.NewStyle().Reverse(true),
|
||||
bracket: lipgloss.NewStyle(),
|
||||
key: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")),
|
||||
null: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")),
|
||||
boolean: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("3")),
|
||||
number: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6")),
|
||||
string: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("2")),
|
||||
preview: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")),
|
||||
statusBar: lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")),
|
||||
search: lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")),
|
||||
}
|
||||
|
||||
func main() {
|
||||
filePath := ""
|
||||
var dec *json.Decoder
|
||||
if term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
if len(os.Args) >= 2 {
|
||||
filePath = os.Args[1]
|
||||
f, err := os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dec = json.NewDecoder(f)
|
||||
}
|
||||
} else {
|
||||
dec = json.NewDecoder(os.Stdin)
|
||||
}
|
||||
dec.UseNumber()
|
||||
jsonObject, err := parse(dec)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
expand := map[string]bool{
|
||||
"": true,
|
||||
}
|
||||
|
||||
if array, ok := jsonObject.(array); ok {
|
||||
for i := range array {
|
||||
expand[accessor("", i)] = true
|
||||
}
|
||||
}
|
||||
|
||||
parents := map[string]string{}
|
||||
dfs(jsonObject, func(it iterator) {
|
||||
parents[it.path] = it.parent
|
||||
})
|
||||
|
||||
input := textinput.New()
|
||||
input.Prompt = ""
|
||||
|
||||
m := &model{
|
||||
fileName: path.Base(filePath),
|
||||
json: jsonObject,
|
||||
width: 80,
|
||||
height: 60,
|
||||
mouseWheelDelta: 3,
|
||||
keyMap: DefaultKeyMap(),
|
||||
expandedPaths: expand,
|
||||
parents: parents,
|
||||
wrap: true,
|
||||
searchInput: input,
|
||||
}
|
||||
|
||||
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||
if err := p.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(m.exitCode)
|
||||
}
|
||||
|
||||
type model struct {
|
||||
exitCode int
|
||||
width, height int
|
||||
windowHeight int
|
||||
footerHeight int
|
||||
|
||||
fileName string
|
||||
json interface{}
|
||||
lines []string
|
||||
|
||||
mouseWheelDelta int // number of lines the mouse wheel will scroll
|
||||
offset int // offset is the vertical scroll position
|
||||
|
||||
keyMap KeyMap
|
||||
showHelp bool
|
||||
|
||||
expandedPaths map[string]bool // a set with expanded paths
|
||||
canBeExpanded map[string]bool // a set for path => can be expanded (i.e. dict or array)
|
||||
pathToLineNumber *dict // dict with path => line number
|
||||
lineNumberToPath map[int]string // map of line number => path
|
||||
parents map[string]string // map of subpath => parent path
|
||||
cursor int // cursor in range of m.pathToLineNumber.keys slice
|
||||
showCursor bool
|
||||
|
||||
wrap bool
|
||||
searchInput textinput.Model
|
||||
searchRegexCompileError string
|
||||
searchResults *dict // path => searchResult
|
||||
showSearchResults bool
|
||||
resultsCursor int // [0, searchResults length)
|
||||
}
|
||||
|
||||
func (m *model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.windowHeight = msg.Height
|
||||
m.searchInput.Width = msg.Width - 2 // minus prompt
|
||||
m.render()
|
||||
|
||||
case tea.MouseMsg:
|
||||
switch msg.Type {
|
||||
case tea.MouseWheelUp:
|
||||
m.LineUp(m.mouseWheelDelta)
|
||||
case tea.MouseWheelDown:
|
||||
m.LineDown(m.mouseWheelDelta)
|
||||
}
|
||||
}
|
||||
|
||||
if m.searchInput.Focused() {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.searchInput.Blur()
|
||||
m.searchResults = newDict()
|
||||
m.render()
|
||||
|
||||
case tea.KeyEnter:
|
||||
m.doSearch(m.searchInput.Value())
|
||||
}
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.searchInput, cmd = m.searchInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keyMap.PageDown):
|
||||
m.ViewDown()
|
||||
case key.Matches(msg, m.keyMap.PageUp):
|
||||
m.ViewUp()
|
||||
case key.Matches(msg, m.keyMap.HalfPageDown):
|
||||
m.HalfViewDown()
|
||||
case key.Matches(msg, m.keyMap.HalfPageUp):
|
||||
m.HalfViewUp()
|
||||
case key.Matches(msg, m.keyMap.GotoTop):
|
||||
m.GotoTop()
|
||||
case key.Matches(msg, m.keyMap.GotoBottom):
|
||||
m.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
if m.showHelp {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keyMap.Quit):
|
||||
m.showHelp = false
|
||||
m.render()
|
||||
case key.Matches(msg, m.keyMap.Down):
|
||||
m.LineDown(1)
|
||||
case key.Matches(msg, m.keyMap.Up):
|
||||
m.LineUp(1)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keyMap.Quit):
|
||||
m.exitCode = 0
|
||||
return m, tea.Quit
|
||||
|
||||
case key.Matches(msg, m.keyMap.Help):
|
||||
m.GotoTop()
|
||||
m.showHelp = !m.showHelp
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.Down):
|
||||
m.showCursor = true
|
||||
if m.cursor < len(m.pathToLineNumber.keys)-1 { // scroll till last element in m.pathToLineNumber
|
||||
m.cursor++
|
||||
} else {
|
||||
// at the bottom of viewport maybe some hidden brackets, lets scroll to see them
|
||||
if !m.AtBottom() {
|
||||
m.LineDown(1)
|
||||
}
|
||||
}
|
||||
if m.cursor >= len(m.pathToLineNumber.keys) {
|
||||
m.cursor = len(m.pathToLineNumber.keys) - 1
|
||||
}
|
||||
m.render()
|
||||
at := m.cursorLineNumber()
|
||||
if m.offset <= at { // cursor is lower
|
||||
m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0
|
||||
} else {
|
||||
m.SetOffset(at)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.keyMap.Up):
|
||||
m.showCursor = true
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
if m.cursor >= len(m.pathToLineNumber.keys) {
|
||||
m.cursor = len(m.pathToLineNumber.keys) - 1
|
||||
}
|
||||
m.render()
|
||||
at := m.cursorLineNumber()
|
||||
if at < m.offset+m.height { // cursor is above
|
||||
m.LineUp(max(0, m.offset-at))
|
||||
} else {
|
||||
m.SetOffset(at)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.keyMap.Expand):
|
||||
m.showCursor = true
|
||||
m.expandedPaths[m.cursorPath()] = true
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.Collapse):
|
||||
m.showCursor = true
|
||||
if m.expandedPaths[m.cursorPath()] {
|
||||
m.expandedPaths[m.cursorPath()] = false
|
||||
} else {
|
||||
parentPath, ok := m.parents[m.cursorPath()]
|
||||
if ok {
|
||||
m.expandedPaths[parentPath] = false
|
||||
index, _ := m.pathToLineNumber.index(parentPath)
|
||||
m.cursor = index
|
||||
}
|
||||
}
|
||||
m.render()
|
||||
at := m.cursorLineNumber()
|
||||
if at < m.offset+m.height { // cursor is above
|
||||
m.LineUp(max(0, m.offset-at))
|
||||
} else {
|
||||
m.SetOffset(at)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.keyMap.ToggleWrap):
|
||||
m.wrap = !m.wrap
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.ExpandAll):
|
||||
dfs(m.json, func(it iterator) {
|
||||
switch it.object.(type) {
|
||||
case *dict, array:
|
||||
m.expandedPaths[it.path] = true
|
||||
}
|
||||
})
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.CollapseAll):
|
||||
m.expandedPaths = map[string]bool{
|
||||
"": true,
|
||||
}
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.Search):
|
||||
m.showSearchResults = false
|
||||
m.searchInput.Focus()
|
||||
m.render()
|
||||
return m, textinput.Blink
|
||||
|
||||
case key.Matches(msg, m.keyMap.Next):
|
||||
if m.showSearchResults {
|
||||
m.nextSearchResult()
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.keyMap.Prev):
|
||||
if m.showSearchResults {
|
||||
m.prevSearchResult()
|
||||
}
|
||||
}
|
||||
|
||||
case tea.MouseMsg:
|
||||
switch msg.Type {
|
||||
case tea.MouseLeft:
|
||||
m.showCursor = true
|
||||
clickedPath, ok := m.lineNumberToPath[m.offset+msg.Y]
|
||||
if ok {
|
||||
if m.canBeExpanded[clickedPath] {
|
||||
m.expandedPaths[clickedPath] = !m.expandedPaths[clickedPath]
|
||||
}
|
||||
index, _ := m.pathToLineNumber.index(clickedPath)
|
||||
m.cursor = index
|
||||
m.render()
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *model) View() string {
|
||||
lines := m.visibleLines()
|
||||
extraLines := ""
|
||||
if len(lines) < m.height {
|
||||
extraLines = strings.Repeat("\n", max(0, m.height-len(lines)))
|
||||
}
|
||||
statusBar := m.cursorPath() + " "
|
||||
if m.showHelp {
|
||||
statusBar = "Press Esc or q to close help."
|
||||
}
|
||||
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)-width(m.fileName)))
|
||||
statusBar += m.fileName
|
||||
statusBar = colors.statusBar.Render(statusBar)
|
||||
output := strings.Join(lines, "\n") + extraLines + "\n" + statusBar
|
||||
if m.searchInput.Focused() {
|
||||
output += "\n/" + m.searchInput.View()
|
||||
}
|
||||
if len(m.searchRegexCompileError) > 0 {
|
||||
output += fmt.Sprintf("\n/%v/i %v", m.searchInput.Value(), m.searchRegexCompileError)
|
||||
}
|
||||
if m.showSearchResults {
|
||||
if len(m.searchResults.keys) == 0 {
|
||||
output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value())
|
||||
} else {
|
||||
output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.resultsCursor+1, len(m.searchResults.keys))
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (m *model) recalculateViewportHeight() {
|
||||
m.height = m.windowHeight
|
||||
m.height-- // status bar
|
||||
if m.searchInput.Focused() {
|
||||
m.height--
|
||||
}
|
||||
if m.showSearchResults {
|
||||
m.height--
|
||||
}
|
||||
if len(m.searchRegexCompileError) > 0 {
|
||||
m.height--
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) render() {
|
||||
m.recalculateViewportHeight()
|
||||
|
||||
if m.showHelp {
|
||||
m.lines = m.helpView()
|
||||
return
|
||||
}
|
||||
|
||||
m.pathToLineNumber = newDict()
|
||||
m.canBeExpanded = map[string]bool{}
|
||||
m.lineNumberToPath = map[int]string{}
|
||||
m.lines = m.print(m.json, 1, 0, 0, "", false)
|
||||
|
||||
if m.offset > len(m.lines)-1 {
|
||||
m.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) cursorPath() string {
|
||||
if m.cursor == 0 {
|
||||
return ""
|
||||
}
|
||||
if 0 <= m.cursor && m.cursor < len(m.pathToLineNumber.keys) {
|
||||
return m.pathToLineNumber.keys[m.cursor]
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
func (m *model) cursorLineNumber() int {
|
||||
if 0 <= m.cursor && m.cursor < len(m.pathToLineNumber.keys) {
|
||||
return m.pathToLineNumber.values[m.pathToLineNumber.keys[m.cursor]].(int)
|
||||
}
|
||||
return -1
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func parse(dec *json.Decoder) (interface{}, error) {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
return decodeDict(dec)
|
||||
case '[':
|
||||
return decodeArray(dec)
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func decodeDict(dec *json.Decoder) (*dict, error) {
|
||||
d := newDict()
|
||||
for {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok && delim == '}' {
|
||||
return d, nil
|
||||
}
|
||||
key := token.(string)
|
||||
token, err = dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var value interface{} = token
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
value, err = decodeDict(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case '[':
|
||||
value, err = decodeArray(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
d.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
type array = []interface{}
|
||||
|
||||
func decodeArray(dec *json.Decoder) ([]interface{}, error) {
|
||||
slice := make(array, 0)
|
||||
for index := 0; ; index++ {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
value, err := decodeDict(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slice = append(slice, value)
|
||||
case '[':
|
||||
value, err := decodeArray(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slice = append(slice, value)
|
||||
case ']':
|
||||
return slice, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
slice = append(slice, token)
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_parse(t *testing.T) {
|
||||
input := `{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"a": 3,
|
||||
"slice": [{"z": "z", "1": "1"}]
|
||||
}`
|
||||
|
||||
p, err := parse(json.NewDecoder(strings.NewReader(input)))
|
||||
if err != nil {
|
||||
t.Error("JSON parse error", err)
|
||||
}
|
||||
o := p.(*dict)
|
||||
|
||||
expectedKeys := []string{
|
||||
"a",
|
||||
"b",
|
||||
"slice",
|
||||
}
|
||||
for i := range o.keys {
|
||||
if o.keys[i] != expectedKeys[i] {
|
||||
t.Error("Wrong key order ", i, o.keys[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
|
||||
s, ok := o.get("slice")
|
||||
if !ok {
|
||||
t.Error("slice missing")
|
||||
}
|
||||
a := s.(array)
|
||||
z := a[0].(*dict)
|
||||
|
||||
expectedKeys = []string{
|
||||
"z",
|
||||
"1",
|
||||
}
|
||||
for i := range z.keys {
|
||||
if z.keys[i] != expectedKeys[i] {
|
||||
t.Error("Wrong key order for nested map ", i, z.keys[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,263 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path string, dontHighlightCursor bool) []string {
|
||||
ident := strings.Repeat(" ", level)
|
||||
subident := strings.Repeat(" ", level-1)
|
||||
|
||||
cursorOr := func(style lipgloss.Style) lipgloss.Style {
|
||||
if m.cursorPath() == path && !dontHighlightCursor && m.showCursor {
|
||||
return colors.cursor
|
||||
}
|
||||
return style
|
||||
}
|
||||
searchStyle := colors.search.Render
|
||||
if m.resultsCursorPath() == path {
|
||||
searchStyle = colors.cursor.Render
|
||||
}
|
||||
highlight := func(line string, style func(s string) string) string {
|
||||
chunks := m.split(line, path)
|
||||
return mergeChunks(chunks, style, searchStyle)
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case nil:
|
||||
line := highlight("null", cursorOr(colors.null).Render)
|
||||
return []string{line}
|
||||
|
||||
case bool:
|
||||
line := highlight(stringify(v), cursorOr(colors.boolean).Render)
|
||||
return []string{line}
|
||||
|
||||
case json.Number:
|
||||
line := highlight(v.(json.Number).String(), cursorOr(colors.number).Render)
|
||||
return []string{line}
|
||||
|
||||
case string:
|
||||
stringStyle := cursorOr(colors.string).Render
|
||||
line := fmt.Sprintf("%q", v)
|
||||
chunks := m.split(line, path)
|
||||
if m.wrap && keyEndPos+width(line) > m.width {
|
||||
return wrapLines(chunks, keyEndPos, m.width, subident, stringStyle, searchStyle)
|
||||
}
|
||||
return []string{mergeChunks(chunks, stringStyle, searchStyle)}
|
||||
|
||||
case *dict:
|
||||
m.pathToLineNumber.set(path, lineNumber)
|
||||
m.canBeExpanded[path] = true
|
||||
m.lineNumberToPath[lineNumber] = path
|
||||
bracketStyle := cursorOr(colors.bracket).Render
|
||||
if len(v.(*dict).keys) == 0 {
|
||||
return []string{bracketStyle("{}")}
|
||||
}
|
||||
if !m.expandedPaths[path] {
|
||||
return []string{m.preview(v, path, dontHighlightCursor)}
|
||||
}
|
||||
output := []string{bracketStyle("{")}
|
||||
lineNumber++ // bracket is on separate line
|
||||
keys := v.(*dict).keys
|
||||
for i, k := range keys {
|
||||
subpath := path + "." + k
|
||||
m.pathToLineNumber.set(subpath, lineNumber)
|
||||
m.lineNumberToPath[lineNumber] = subpath
|
||||
keyStyle := colors.key.Render
|
||||
if m.cursorPath() == subpath && m.showCursor {
|
||||
keyStyle = colors.cursor.Render
|
||||
}
|
||||
key := fmt.Sprintf("%q", k)
|
||||
{
|
||||
var indexes [][]int
|
||||
if m.searchResults != nil {
|
||||
sr, ok := m.searchResults.get(subpath)
|
||||
if ok {
|
||||
indexes = sr.(searchResult).key
|
||||
}
|
||||
}
|
||||
chunks := explode(key, indexes)
|
||||
searchStyle := colors.search.Render
|
||||
if m.resultsCursorPath() == subpath && !m.showCursor {
|
||||
searchStyle = colors.cursor.Render
|
||||
}
|
||||
key = mergeChunks(chunks, keyStyle, searchStyle)
|
||||
}
|
||||
value, _ := v.(*dict).get(k)
|
||||
delim := ": "
|
||||
keyEndPos := width(ident) + width(key) + width(delim)
|
||||
lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, true)
|
||||
lines[0] = ident + key + delim + lines[0]
|
||||
if i < len(keys)-1 {
|
||||
lines[len(lines)-1] += ","
|
||||
}
|
||||
output = append(output, lines...)
|
||||
lineNumber += len(lines)
|
||||
}
|
||||
output = append(output, subident+colors.bracket.Render("}"))
|
||||
return output
|
||||
|
||||
case array:
|
||||
m.pathToLineNumber.set(path, lineNumber)
|
||||
m.canBeExpanded[path] = true
|
||||
m.lineNumberToPath[lineNumber] = path
|
||||
bracketStyle := cursorOr(colors.bracket).Render
|
||||
if len(v.(array)) == 0 {
|
||||
return []string{bracketStyle("[]")}
|
||||
}
|
||||
if !m.expandedPaths[path] {
|
||||
return []string{bracketStyle(m.preview(v, path, dontHighlightCursor))}
|
||||
}
|
||||
output := []string{bracketStyle("[")}
|
||||
lineNumber++ // bracket is on separate line
|
||||
slice := v.(array)
|
||||
for i, value := range slice {
|
||||
subpath := fmt.Sprintf("%v[%v]", path, i)
|
||||
m.pathToLineNumber.set(subpath, lineNumber)
|
||||
m.lineNumberToPath[lineNumber] = subpath
|
||||
lines := m.print(value, level+1, lineNumber, width(ident), subpath, false)
|
||||
lines[0] = ident + lines[0]
|
||||
if i < len(slice)-1 {
|
||||
lines[len(lines)-1] += ","
|
||||
}
|
||||
lineNumber += len(lines)
|
||||
output = append(output, lines...)
|
||||
}
|
||||
output = append(output, subident+colors.bracket.Render("]"))
|
||||
return output
|
||||
|
||||
default:
|
||||
return []string{"unknown type"}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) preview(v interface{}, path string, dontHighlightCursor bool) string {
|
||||
cursorOr := func(style lipgloss.Style) lipgloss.Style {
|
||||
if m.cursorPath() == path && !dontHighlightCursor {
|
||||
return colors.cursor
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
bracketStyle := cursorOr(colors.bracket)
|
||||
previewStyle := cursorOr(colors.preview)
|
||||
|
||||
printValue := func(value interface{}) string {
|
||||
switch value.(type) {
|
||||
case nil, bool, json.Number:
|
||||
return previewStyle.Render(fmt.Sprintf("%v", value))
|
||||
case string:
|
||||
return previewStyle.Render(fmt.Sprintf("%q", value))
|
||||
case *dict:
|
||||
return previewStyle.Render("{\u2026}")
|
||||
case array:
|
||||
return previewStyle.Render("[\u2026]")
|
||||
}
|
||||
return "..."
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case *dict:
|
||||
output := bracketStyle.Render("{")
|
||||
keys := v.(*dict).keys
|
||||
for _, k := range keys {
|
||||
key := fmt.Sprintf("%q", k)
|
||||
output += previewStyle.Render(key + ": ")
|
||||
value, _ := v.(*dict).get(k)
|
||||
output += printValue(value)
|
||||
break
|
||||
}
|
||||
if len(keys) == 1 {
|
||||
output += bracketStyle.Render("}")
|
||||
} else {
|
||||
output += bracketStyle.Render(", \u2026}")
|
||||
}
|
||||
return output
|
||||
|
||||
case array:
|
||||
output := bracketStyle.Render("[")
|
||||
slice := v.(array)
|
||||
for _, value := range slice {
|
||||
output += printValue(value)
|
||||
break
|
||||
}
|
||||
if len(slice) == 1 {
|
||||
output += bracketStyle.Render("]")
|
||||
} else {
|
||||
output += bracketStyle.Render(", \u2026]")
|
||||
}
|
||||
return output
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
func wrapLines(chunks []string, keyEndPos, mWidth int, subident string, stringStyle, searchStyle func(s string) string) []string {
|
||||
wrappedLines := make([]string, 0)
|
||||
currentLine := ""
|
||||
ident := "" // First line stays on the same line with a "key",
|
||||
pos := keyEndPos // so no ident is needed. Start counting from the "key" offset.
|
||||
style := stringStyle
|
||||
for i, chunk := range chunks {
|
||||
if i%2 == 0 {
|
||||
style = stringStyle
|
||||
} else {
|
||||
style = searchStyle
|
||||
}
|
||||
buffer := ""
|
||||
for _, ch := range chunk {
|
||||
buffer += string(ch)
|
||||
if pos == mWidth-1 {
|
||||
wrappedLines = append(wrappedLines, ident+currentLine+style(buffer))
|
||||
currentLine = ""
|
||||
buffer = ""
|
||||
pos = width(subident) // Start counting from ident.
|
||||
ident = subident // After first line, add ident to all.
|
||||
} else {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
currentLine += style(buffer)
|
||||
}
|
||||
if width(currentLine) > 0 {
|
||||
wrappedLines = append(wrappedLines, subident+currentLine)
|
||||
}
|
||||
return wrappedLines
|
||||
}
|
||||
|
||||
func (m *model) split(line, path string) []string {
|
||||
var indexes [][]int
|
||||
if m.searchResults != nil {
|
||||
sr, ok := m.searchResults.get(path)
|
||||
if ok {
|
||||
indexes = sr.(searchResult).value
|
||||
}
|
||||
}
|
||||
return explode(line, indexes)
|
||||
}
|
||||
|
||||
func explode(s string, indexes [][]int) []string {
|
||||
out := make([]string, 0)
|
||||
pos := 0
|
||||
for _, l := range indexes {
|
||||
out = append(out, s[pos:l[0]])
|
||||
out = append(out, s[l[0]:l[1]])
|
||||
pos = l[1]
|
||||
}
|
||||
out = append(out, s[pos:])
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeChunks(chunks []string, stringStyle, searchStyle func(s string) string) string {
|
||||
currentLine := ""
|
||||
for i, chunk := range chunks {
|
||||
if i%2 == 0 {
|
||||
currentLine += stringStyle(chunk)
|
||||
} else {
|
||||
currentLine += searchStyle(chunk)
|
||||
}
|
||||
}
|
||||
return currentLine
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type searchResult struct {
|
||||
key, value [][]int
|
||||
}
|
||||
|
||||
func (m *model) doSearch(s string) {
|
||||
re, err := regexp.Compile("(?i)" + s)
|
||||
if err != nil {
|
||||
m.searchRegexCompileError = err.Error()
|
||||
m.searchInput.Blur()
|
||||
return
|
||||
}
|
||||
m.searchRegexCompileError = ""
|
||||
results := newDict()
|
||||
addSearchResult := func(path string, indexes [][]int) {
|
||||
if indexes != nil {
|
||||
sr := searchResult{}
|
||||
prev, ok := results.get(path)
|
||||
if ok {
|
||||
sr = prev.(searchResult)
|
||||
}
|
||||
sr.value = indexes
|
||||
results.set(path, sr)
|
||||
}
|
||||
}
|
||||
|
||||
dfs(m.json, func(it iterator) {
|
||||
switch it.object.(type) {
|
||||
case nil:
|
||||
line := "null"
|
||||
found := re.FindAllStringIndex(line, -1)
|
||||
addSearchResult(it.path, found)
|
||||
case bool:
|
||||
line := stringify(it.object)
|
||||
found := re.FindAllStringIndex(line, -1)
|
||||
addSearchResult(it.path, found)
|
||||
case json.Number:
|
||||
line := it.object.(json.Number).String()
|
||||
found := re.FindAllStringIndex(line, -1)
|
||||
addSearchResult(it.path, found)
|
||||
case string:
|
||||
line := fmt.Sprintf("%q", it.object)
|
||||
found := re.FindAllStringIndex(line, -1)
|
||||
addSearchResult(it.path, found)
|
||||
case *dict:
|
||||
keys := it.object.(*dict).keys
|
||||
for _, key := range keys {
|
||||
line := fmt.Sprintf("%q", key)
|
||||
subpath := it.path + "." + key
|
||||
indexes := re.FindAllStringIndex(line, -1)
|
||||
if indexes != nil {
|
||||
sr := searchResult{}
|
||||
prev, ok := results.get(subpath)
|
||||
if ok {
|
||||
sr = prev.(searchResult)
|
||||
}
|
||||
sr.key = indexes
|
||||
results.set(subpath, sr)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
m.searchResults = results
|
||||
m.searchInput.Blur()
|
||||
m.showSearchResults = true
|
||||
m.jumpToSearchResult(0)
|
||||
}
|
||||
|
||||
func (m *model) jumpToSearchResult(at int) {
|
||||
if m.searchResults == nil || len(m.searchResults.keys) == 0 {
|
||||
return
|
||||
}
|
||||
m.showCursor = false
|
||||
m.resultsCursor = at % len(m.searchResults.keys)
|
||||
desiredPath := m.searchResults.keys[m.resultsCursor]
|
||||
lineNumber, ok := m.pathToLineNumber.get(desiredPath)
|
||||
if ok {
|
||||
m.cursor = m.pathToLineNumber.indexes[desiredPath]
|
||||
m.SetOffset(lineNumber.(int))
|
||||
m.render()
|
||||
} else {
|
||||
m.recursiveExpand(desiredPath)
|
||||
m.render()
|
||||
m.jumpToSearchResult(at)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) recursiveExpand(path string) {
|
||||
m.expandedPaths[path] = true
|
||||
if path != "" {
|
||||
m.recursiveExpand(m.parents[path])
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) nextSearchResult() {
|
||||
m.jumpToSearchResult((m.resultsCursor + 1) % len(m.searchResults.keys))
|
||||
}
|
||||
|
||||
func (m *model) prevSearchResult() {
|
||||
i := m.resultsCursor - 1
|
||||
if i < 0 {
|
||||
i = len(m.searchResults.keys) - 1
|
||||
}
|
||||
m.jumpToSearchResult(i)
|
||||
}
|
||||
|
||||
func (m *model) resultsCursorPath() string {
|
||||
if m.searchResults == nil || len(m.searchResults.keys) == 0 {
|
||||
return "?"
|
||||
}
|
||||
return m.searchResults.keys[m.resultsCursor]
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_splitByFoundIndexes(t *testing.T) {
|
||||
s := fmt.Sprintf("%q", "0 aaa 123 \"bbb 44\" ccc 5")
|
||||
re := regexp.MustCompile("\"?\\d+\"?")
|
||||
indexes := re.FindAllStringIndex(s, -1)
|
||||
chunks := explode(s, indexes)
|
||||
expected := []string{"", "\"0", " aaa ", "123", " \\\"bbb ", "44", "\\\" ccc ", "5\"", ""}
|
||||
ok := reflect.DeepEqual(chunks, expected)
|
||||
if !ok {
|
||||
t.Errorf(
|
||||
"split error:\n"+
|
||||
" got %v,\n"+
|
||||
" expected %v",
|
||||
stringify(chunks),
|
||||
stringify(expected),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_splitByFoundIndexes_empty(t *testing.T) {
|
||||
s := fmt.Sprintf("%q", "foo")
|
||||
chunks := explode(s, nil)
|
||||
expected := []string{"\"foo\""}
|
||||
ok := reflect.DeepEqual(chunks, expected)
|
||||
if !ok {
|
||||
t.Errorf(
|
||||
"split error:\n"+
|
||||
" got %v,\n"+
|
||||
" expected %v",
|
||||
stringify(chunks),
|
||||
stringify(expected),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
type iterator struct {
|
||||
object interface{}
|
||||
path, parent string
|
||||
}
|
||||
|
||||
func dfs(object interface{}, f func(it iterator)) {
|
||||
sub(iterator{object: object}, f)
|
||||
}
|
||||
|
||||
func sub(it iterator, f func(it iterator)) {
|
||||
f(it)
|
||||
switch it.object.(type) {
|
||||
case *dict:
|
||||
keys := it.object.(*dict).keys
|
||||
for _, k := range keys {
|
||||
subpath := it.path + "." + k
|
||||
value, _ := it.object.(*dict).get(k)
|
||||
sub(iterator{
|
||||
object: value,
|
||||
path: subpath,
|
||||
parent: it.path,
|
||||
}, f)
|
||||
}
|
||||
|
||||
case array:
|
||||
slice := it.object.(array)
|
||||
for i, value := range slice {
|
||||
subpath := accessor(it.path, i)
|
||||
sub(iterator{
|
||||
object: value,
|
||||
path: subpath,
|
||||
parent: it.path,
|
||||
}, f)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func clamp(v, low, high int) int {
|
||||
if high < low {
|
||||
low, high = high, low
|
||||
}
|
||||
return min(high, max(low, v))
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func stringify(x interface{}) string {
|
||||
str, err := json.Marshal(x)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(str)
|
||||
}
|
||||
|
||||
func width(s string) int {
|
||||
return lipgloss.Width(s)
|
||||
}
|
||||
|
||||
func accessor(path string, to interface{}) string {
|
||||
return fmt.Sprintf("%v[%v]", path, to)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_clamp(t *testing.T) {
|
||||
got := clamp(1, 2, 3)
|
||||
if got != 2 {
|
||||
t.Errorf("clamp() = %v, want 2", got)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_max(t *testing.T) {
|
||||
got := max(1, 2)
|
||||
if got != 2 {
|
||||
t.Errorf("max() = %v, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_min(t *testing.T) {
|
||||
got := min(1, 2)
|
||||
if got != 1 {
|
||||
t.Errorf("min() = %v, want 1", got)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
func (m *model) AtTop() bool {
|
||||
return m.offset <= 0
|
||||
}
|
||||
|
||||
func (m *model) AtBottom() bool {
|
||||
return m.offset >= m.maxYOffset()
|
||||
}
|
||||
|
||||
func (m *model) PastBottom() bool {
|
||||
return m.offset > m.maxYOffset()
|
||||
}
|
||||
|
||||
func (m *model) ScrollPercent() float64 {
|
||||
if m.height >= len(m.lines) {
|
||||
return 1.0
|
||||
}
|
||||
y := float64(m.offset)
|
||||
h := float64(m.height)
|
||||
t := float64(len(m.lines) - 1)
|
||||
v := y / (t - h)
|
||||
return math.Max(0.0, math.Min(1.0, v))
|
||||
}
|
||||
|
||||
func (m *model) maxYOffset() int {
|
||||
return max(0, len(m.lines)-m.height)
|
||||
}
|
||||
|
||||
func (m *model) visibleLines() (lines []string) {
|
||||
if len(m.lines) > 0 {
|
||||
top := max(0, m.offset)
|
||||
bottom := clamp(m.offset+m.height, top, len(m.lines))
|
||||
lines = m.lines[top:bottom]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m *model) SetOffset(n int) {
|
||||
m.offset = clamp(n, 0, m.maxYOffset())
|
||||
}
|
||||
|
||||
func (m *model) ViewDown() {
|
||||
if m.AtBottom() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset + m.height)
|
||||
}
|
||||
|
||||
func (m *model) ViewUp() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset - m.height)
|
||||
}
|
||||
|
||||
func (m *model) HalfViewDown() {
|
||||
if m.AtBottom() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset + m.height/2)
|
||||
}
|
||||
|
||||
func (m *model) HalfViewUp() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset - m.height/2)
|
||||
}
|
||||
|
||||
func (m *model) LineDown(n int) {
|
||||
if m.AtBottom() || n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the number of lines by which we're going to scroll isn't
|
||||
// greater than the number of lines we actually have left before we reach
|
||||
// the bottom.
|
||||
m.SetOffset(m.offset + n)
|
||||
}
|
||||
|
||||
func (m *model) LineUp(n int) {
|
||||
if m.AtTop() || n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the number of lines by which we're going to scroll isn't
|
||||
// greater than the number of lines we are from the top.
|
||||
m.SetOffset(m.offset - n)
|
||||
}
|
||||
|
||||
func (m *model) GotoTop() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(0)
|
||||
}
|
||||
|
||||
func (m *model) GotoBottom() {
|
||||
m.SetOffset(m.maxYOffset())
|
||||
}
|
Loading…
Reference in New Issue