You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fx/main.go

539 lines
13 KiB
Go

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"
)
type number = json.Number
var colors = struct {
cursor lipgloss.Style
syntax 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),
syntax: 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 args []string
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)
args = os.Args[2:]
}
} else {
dec = json.NewDecoder(os.Stdin)
args = os.Args[1:]
}
dec.UseNumber()
jsonObject, err := parse(dec)
if err != nil {
panic(err)
}
if len(args) > 0 {
if args[0] == "--print-code" {
fmt.Print(generateCode(args[1:]))
return
}
reduce(jsonObject, args)
return
}
expand := map[string]bool{
"": true,
}
if array, ok := jsonObject.(array); ok {
for i := range array {
expand[accessor("", i)] = true
}
}
parents := map[string]string{}
children := map[string][]string{}
canBeExpanded := map[string]bool{}
dfs(jsonObject, func(it iterator) {
parents[it.path] = it.parent
children[it.parent] = append(children[it.parent], it.path)
switch it.object.(type) {
case *dict:
canBeExpanded[it.path] = len(it.object.(*dict).keys) > 0
case array:
canBeExpanded[it.path] = len(it.object.(array)) > 0
}
})
input := textinput.New()
input.Prompt = ""
m := &model{
fileName: path.Base(filePath),
json: jsonObject,
width: 80,
height: 60,
mouseWheelDelta: 3,
keyMap: DefaultKeyMap(),
expandedPaths: expand,
canBeExpanded: canBeExpanded,
parents: parents,
children: children,
nextSiblings: map[string]string{},
prevSiblings: map[string]string{},
wrap: true,
searchInput: input,
}
m.collectSiblings(m.json, "")
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
wrap bool
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
children map[string][]string // map of path => child paths
nextSiblings, prevSiblings map[string]string // map of path => sibling path
cursor int // cursor in range of m.pathToLineNumber.keys slice
showCursor bool
searchInput textinput.Model
searchRegexCompileError string
showSearchResults bool
searchResults []*searchResult
searchResultsCursor int
searchResultsIndex map[string]searchResultGroup
}
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.down()
m.render()
m.scrollDownToCursor()
case key.Matches(msg, m.keyMap.Up):
m.up()
m.render()
m.scrollUpToCursor()
case key.Matches(msg, m.keyMap.NextSibling):
nextSiblingPath, ok := m.nextSiblings[m.cursorPath()]
if ok {
index, _ := m.pathToLineNumber.indexes[nextSiblingPath]
m.showCursor = true
m.cursor = index
} else {
m.down()
}
m.render()
m.scrollDownToCursor()
case key.Matches(msg, m.keyMap.PrevSibling):
prevSiblingPath, ok := m.prevSiblings[m.cursorPath()]
if ok {
index, _ := m.pathToLineNumber.indexes[prevSiblingPath]
m.showCursor = true
m.cursor = index
} else {
m.up()
}
m.render()
m.scrollUpToCursor()
case key.Matches(msg, m.keyMap.Expand):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] {
m.expandedPaths[m.cursorPath()] = true
}
m.render()
case key.Matches(msg, m.keyMap.ExpandRecursively):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] {
m.expandRecursively(m.cursorPath())
}
m.render()
case key.Matches(msg, m.keyMap.Collapse):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] && 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()
m.scrollUpToCursor()
case key.Matches(msg, m.keyMap.CollapseRecursively):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] {
m.collapseRecursively(m.cursorPath())
} else {
parentPath, ok := m.parents[m.cursorPath()]
if ok {
m.collapseRecursively(parentPath)
index, _ := m.pathToLineNumber.index(parentPath)
m.cursor = index
}
}
m.render()
m.scrollUpToCursor()
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)))
}
if m.showHelp {
statusBar := "Press Esc or q to close help."
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)))
statusBar = colors.statusBar.Render(statusBar)
return strings.Join(lines, "\n") + extraLines + "\n" + statusBar
}
statusBar := m.cursorPath() + " "
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.searchResultsCursor+1, len(m.searchResults.keys))
// }
//}
return output
}
func (m *model) recalculateViewportHeight() {
m.height = m.windowHeight
m.height-- // status bar
if !m.showHelp {
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
}
if m.pathToLineNumber == nil {
m.pathToLineNumber = newDict()
} else {
m.pathToLineNumber = newDictOfCapacity(cap(m.pathToLineNumber.keys))
}
if m.lineNumberToPath == nil {
m.lineNumberToPath = make(map[int]string, 0)
} else {
m.lineNumberToPath = make(map[int]string, len(m.lineNumberToPath))
}
m.lines = m.print(m.json, 1, 0, 0, "", true)
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
}
func (m *model) expandRecursively(path string) {
if m.canBeExpanded[path] {
m.expandedPaths[path] = true
for _, childPath := range m.children[path] {
if childPath != "" {
m.expandRecursively(childPath)
}
}
}
}
func (m *model) collapseRecursively(path string) {
m.expandedPaths[path] = false
for _, childPath := range m.children[path] {
if childPath != "" {
m.collapseRecursively(childPath)
}
}
}
func (m *model) collectSiblings(v interface{}, path string) {
switch v.(type) {
case *dict:
prev := ""
for _, k := range v.(*dict).keys {
subpath := path + "." + k
if prev != "" {
m.nextSiblings[prev] = subpath
m.prevSiblings[subpath] = prev
}
prev = subpath
value, _ := v.(*dict).get(k)
m.collectSiblings(value, subpath)
}
case array:
prev := ""
for i, value := range v.(array) {
subpath := fmt.Sprintf("%v[%v]", path, i)
if prev != "" {
m.nextSiblings[prev] = subpath
m.prevSiblings[subpath] = prev
}
prev = subpath
m.collectSiblings(value, subpath)
}
}
}
func (m *model) 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
}
}
func (m *model) up() {
m.showCursor = true
if m.cursor > 0 {
m.cursor--
}
if m.cursor >= len(m.pathToLineNumber.keys) {
m.cursor = len(m.pathToLineNumber.keys) - 1
}
}