Reimplement in go

sleep-stdin-bug
Anton Medvedev 2 years ago
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…
Cancel
Save