fx/main.go

562 lines
13 KiB
Go
Raw Normal View History

2022-03-11 15:15:24 +00:00
package main
import (
"encoding/json"
"fmt"
2022-04-17 20:57:12 +00:00
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"github.com/antonmedv/fx/pkg/reducer"
. "github.com/antonmedv/fx/pkg/theme"
2022-03-11 15:15:24 +00:00
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
2022-04-16 19:50:46 +00:00
"github.com/mattn/go-isatty"
"github.com/muesli/termenv"
2022-03-11 15:15:24 +00:00
"golang.org/x/term"
2022-04-17 20:57:12 +00:00
"io/fs"
2022-03-11 15:15:24 +00:00
"os"
"path"
2022-04-14 12:30:36 +00:00
"runtime/pprof"
2022-03-11 15:15:24 +00:00
"strings"
)
func main() {
2022-04-14 12:30:36 +00:00
cpuProfile := os.Getenv("CPU_PROFILE")
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
panic(err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
panic(err)
}
}
2022-04-16 19:50:46 +00:00
themeId, ok := os.LookupEnv("FX_THEME")
if !ok {
themeId = "1"
}
2022-04-17 20:57:12 +00:00
theme, ok := Themes[themeId]
2022-04-16 19:50:46 +00:00
if !ok {
2022-04-17 20:57:12 +00:00
theme = Themes["1"]
2022-04-16 19:50:46 +00:00
}
if termenv.ColorProfile() == termenv.Ascii {
2022-04-17 20:57:12 +00:00
theme = Themes["0"]
2022-04-16 19:50:46 +00:00
}
2022-03-11 15:15:24 +00:00
filePath := ""
2022-03-29 16:13:41 +00:00
var args []string
2022-03-11 15:15:24 +00:00
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 {
switch err.(type) {
case *fs.PathError:
fmt.Println(err)
os.Exit(1)
default:
panic(err)
}
2022-03-11 15:15:24 +00:00
}
dec = json.NewDecoder(f)
2022-03-28 20:13:48 +00:00
args = os.Args[2:]
2022-03-11 15:15:24 +00:00
}
} else {
dec = json.NewDecoder(os.Stdin)
2022-03-28 20:13:48 +00:00
args = os.Args[1:]
2022-03-11 15:15:24 +00:00
}
if dec == nil {
fmt.Println("No input provided. Usage: `fx data.json` or `curl ... | fx`")
os.Exit(1)
}
2022-03-11 15:15:24 +00:00
dec.UseNumber()
2022-04-17 20:57:12 +00:00
jsonObject, err := Parse(dec)
2022-03-11 15:15:24 +00:00
if err != nil {
panic(err)
}
2022-04-16 19:50:46 +00:00
tty := isatty.IsTerminal(os.Stdout.Fd())
if len(args) > 0 || !tty {
if len(args) > 0 && args[0] == "--print-code" {
2022-04-17 20:57:12 +00:00
fmt.Print(reducer.GenerateCode(args[1:]))
2022-03-28 20:13:48 +00:00
return
}
2022-04-17 20:57:12 +00:00
reducer.Reduce(jsonObject, args, theme)
2022-03-28 20:13:48 +00:00
return
}
2022-03-11 15:15:24 +00:00
expand := map[string]bool{
"": true,
}
2022-04-17 20:57:12 +00:00
if array, ok := jsonObject.(Array); ok {
2022-03-11 15:15:24 +00:00
for i := range array {
expand[accessor("", i)] = true
}
}
parents := map[string]string{}
2022-03-28 20:13:48 +00:00
children := map[string][]string{}
canBeExpanded := map[string]bool{}
2022-04-17 20:57:12 +00:00
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
2022-03-28 20:13:48 +00:00
}
})
2022-04-16 19:50:46 +00:00
2022-03-11 15:15:24 +00:00
input := textinput.New()
input.Prompt = ""
2022-04-16 19:50:46 +00:00
2022-03-11 15:15:24 +00:00
m := &model{
fileName: path.Base(filePath),
2022-04-16 19:50:46 +00:00
theme: theme,
2022-03-11 15:15:24 +00:00
json: jsonObject,
width: 80,
height: 60,
mouseWheelDelta: 3,
keyMap: DefaultKeyMap(),
expandedPaths: expand,
2022-03-28 20:13:48 +00:00
canBeExpanded: canBeExpanded,
2022-03-11 15:15:24 +00:00
parents: parents,
2022-03-28 20:13:48 +00:00
children: children,
2022-03-29 16:13:41 +00:00
nextSiblings: map[string]string{},
prevSiblings: map[string]string{},
2022-03-11 15:15:24 +00:00
wrap: true,
searchInput: input,
}
2022-03-29 16:13:41 +00:00
m.collectSiblings(m.json, "")
2022-03-11 15:15:24 +00:00
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
if err := p.Start(); err != nil {
panic(err)
}
2022-04-14 12:30:36 +00:00
if cpuProfile != "" {
pprof.StopCPUProfile()
}
2022-04-07 08:32:34 +00:00
os.Exit(m.exitCode)
2022-03-11 15:15:24 +00:00
}
type model struct {
exitCode int
width, height int
windowHeight int
footerHeight int
2022-03-29 16:13:41 +00:00
wrap bool
2022-04-16 19:50:46 +00:00
theme Theme
2022-03-11 15:15:24 +00:00
fileName string
json interface{}
lines []string
2022-04-17 20:57:12 +00:00
mouseWheelDelta int // Number of lines the mouse wheel will scroll
2022-03-11 15:15:24 +00:00
offset int // offset is the vertical scroll position
keyMap KeyMap
showHelp bool
2022-04-02 20:59:36 +00:00
expandedPaths map[string]bool // set of expanded paths
canBeExpanded map[string]bool // set of path => can be expanded (i.e. dict or array)
paths []string // array of paths on screen
2022-04-17 20:57:12 +00:00
pathToLineNumber map[string]int // map of path => line Number
2022-04-02 20:59:36 +00:00
pathToIndex map[string]int // map of path => index in m.paths
2022-04-17 20:57:12 +00:00
lineNumberToPath map[int]string // map of line Number => path
2022-03-29 16:13:41 +00:00
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
2022-04-02 20:59:36 +00:00
cursor int // cursor in [0, len(m.paths)]
2022-03-29 16:13:41 +00:00
showCursor bool
2022-03-11 15:15:24 +00:00
searchInput textinput.Model
searchRegexCompileError string
showSearchResults bool
2022-03-29 16:13:41 +00:00
searchResults []*searchResult
searchResultsCursor int
2022-04-07 08:32:34 +00:00
highlightIndex map[string]*rangeGroup
2022-03-11 15:15:24 +00:00
}
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()
2022-04-14 12:30:36 +00:00
m.clearSearchResults()
2022-03-11 15:15:24 +00:00
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):
2022-03-29 16:13:41 +00:00
m.down()
2022-03-11 15:15:24 +00:00
m.render()
2022-03-29 16:13:41 +00:00
m.scrollDownToCursor()
2022-03-11 15:15:24 +00:00
2022-03-29 16:13:41 +00:00
case key.Matches(msg, m.keyMap.Up):
m.up()
2022-03-28 20:13:48 +00:00
m.render()
2022-03-29 16:13:41 +00:00
m.scrollUpToCursor()
2022-03-28 20:13:48 +00:00
2022-03-29 16:13:41 +00:00
case key.Matches(msg, m.keyMap.NextSibling):
nextSiblingPath, ok := m.nextSiblings[m.cursorPath()]
if ok {
m.showCursor = true
2022-04-02 20:59:36 +00:00
m.cursor = m.pathToIndex[nextSiblingPath]
2022-03-29 16:13:41 +00:00
} else {
m.down()
2022-03-11 15:15:24 +00:00
}
m.render()
2022-03-29 16:13:41 +00:00
m.scrollDownToCursor()
case key.Matches(msg, m.keyMap.PrevSibling):
prevSiblingPath, ok := m.prevSiblings[m.cursorPath()]
if ok {
m.showCursor = true
2022-04-02 20:59:36 +00:00
m.cursor = m.pathToIndex[prevSiblingPath]
2022-03-11 15:15:24 +00:00
} else {
2022-03-29 16:13:41 +00:00
m.up()
2022-03-11 15:15:24 +00:00
}
2022-03-29 16:13:41 +00:00
m.render()
m.scrollUpToCursor()
2022-03-11 15:15:24 +00:00
case key.Matches(msg, m.keyMap.Expand):
m.showCursor = true
2022-03-29 16:13:41 +00:00
if m.canBeExpanded[m.cursorPath()] {
m.expandedPaths[m.cursorPath()] = true
}
2022-03-11 15:15:24 +00:00
m.render()
2022-03-28 20:13:48 +00:00
case key.Matches(msg, m.keyMap.ExpandRecursively):
m.showCursor = true
2022-03-29 16:13:41 +00:00
if m.canBeExpanded[m.cursorPath()] {
m.expandRecursively(m.cursorPath())
}
2022-03-28 20:13:48 +00:00
m.render()
2022-03-29 16:13:41 +00:00
case key.Matches(msg, m.keyMap.Collapse):
2022-03-11 15:15:24 +00:00
m.showCursor = true
2022-03-29 16:13:41 +00:00
if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] {
m.expandedPaths[m.cursorPath()] = false
2022-03-11 15:15:24 +00:00
} else {
parentPath, ok := m.parents[m.cursorPath()]
if ok {
2022-03-29 16:13:41 +00:00
m.expandedPaths[parentPath] = false
2022-04-02 20:59:36 +00:00
m.cursor = m.pathToIndex[parentPath]
2022-03-11 15:15:24 +00:00
}
}
m.render()
2022-03-29 16:13:41 +00:00
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())
2022-03-11 15:15:24 +00:00
} else {
2022-03-29 16:13:41 +00:00
parentPath, ok := m.parents[m.cursorPath()]
if ok {
m.collapseRecursively(parentPath)
2022-04-02 20:59:36 +00:00
m.cursor = m.pathToIndex[parentPath]
2022-03-29 16:13:41 +00:00
}
2022-03-11 15:15:24 +00:00
}
2022-03-29 16:13:41 +00:00
m.render()
m.scrollUpToCursor()
2022-03-11 15:15:24 +00:00
case key.Matches(msg, m.keyMap.ToggleWrap):
m.wrap = !m.wrap
m.render()
case key.Matches(msg, m.keyMap.ExpandAll):
2022-04-17 20:57:12 +00:00
Dfs(m.json, func(it Iterator) {
switch it.Object.(type) {
case *Dict, Array:
m.expandedPaths[it.Path] = true
2022-03-11 15:15:24 +00:00
}
})
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
2022-04-14 12:30:36 +00:00
m.searchRegexCompileError = ""
2022-03-11 15:15:24 +00:00
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]
}
2022-04-02 20:59:36 +00:00
m.cursor = m.pathToIndex[clickedPath]
2022-03-11 15:15:24 +00:00
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 {
2022-03-29 16:13:41 +00:00
statusBar := "Press Esc or q to close help."
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)))
2022-04-17 20:57:12 +00:00
statusBar = m.theme.StatusBar(statusBar)
2022-03-29 16:13:41 +00:00
return strings.Join(lines, "\n") + extraLines + "\n" + statusBar
2022-03-11 15:15:24 +00:00
}
2022-03-29 16:13:41 +00:00
statusBar := m.cursorPath() + " "
2022-03-11 15:15:24 +00:00
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)-width(m.fileName)))
statusBar += m.fileName
2022-04-17 20:57:12 +00:00
statusBar = m.theme.StatusBar(statusBar)
2022-03-11 15:15:24 +00:00
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)
}
2022-04-14 12:30:36 +00:00
if m.showSearchResults {
if len(m.searchResults) == 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))
}
}
2022-03-11 15:15:24 +00:00
return output
}
func (m *model) recalculateViewportHeight() {
m.height = m.windowHeight
m.height-- // status bar
2022-03-29 16:13:41 +00:00
if !m.showHelp {
if m.searchInput.Focused() {
m.height--
}
if m.showSearchResults {
m.height--
}
if len(m.searchRegexCompileError) > 0 {
m.height--
}
2022-03-11 15:15:24 +00:00
}
}
func (m *model) render() {
m.recalculateViewportHeight()
if m.showHelp {
m.lines = m.helpView()
return
}
2022-04-02 20:59:36 +00:00
m.paths = make([]string, 0)
m.pathToIndex = make(map[string]int, 0)
2022-03-29 16:13:41 +00:00
if m.pathToLineNumber == nil {
2022-04-02 20:59:36 +00:00
m.pathToLineNumber = make(map[string]int, 0)
2022-03-29 16:13:41 +00:00
} else {
2022-04-02 20:59:36 +00:00
m.pathToLineNumber = make(map[string]int, len(m.pathToLineNumber))
2022-03-29 16:13:41 +00:00
}
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)
2022-03-11 15:15:24 +00:00
if m.offset > len(m.lines)-1 {
m.GotoBottom()
}
}
func (m *model) cursorPath() string {
if m.cursor == 0 {
return ""
}
2022-04-02 20:59:36 +00:00
if 0 <= m.cursor && m.cursor < len(m.paths) {
return m.paths[m.cursor]
2022-03-11 15:15:24 +00:00
}
return "?"
}
func (m *model) cursorLineNumber() int {
2022-04-02 20:59:36 +00:00
if 0 <= m.cursor && m.cursor < len(m.paths) {
return m.pathToLineNumber[m.paths[m.cursor]]
2022-03-11 15:15:24 +00:00
}
return -1
}
2022-03-28 20:13:48 +00:00
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)
}
}
}
2022-03-29 16:13:41 +00:00
func (m *model) collectSiblings(v interface{}, path string) {
switch v.(type) {
2022-04-17 20:57:12 +00:00
case *Dict:
2022-03-29 16:13:41 +00:00
prev := ""
2022-04-17 20:57:12 +00:00
for _, k := range v.(*Dict).Keys {
2022-03-29 16:13:41 +00:00
subpath := path + "." + k
if prev != "" {
m.nextSiblings[prev] = subpath
m.prevSiblings[subpath] = prev
}
prev = subpath
2022-04-17 20:57:12 +00:00
value, _ := v.(*Dict).Get(k)
2022-03-29 16:13:41 +00:00
m.collectSiblings(value, subpath)
}
2022-04-17 20:57:12 +00:00
case Array:
2022-03-29 16:13:41 +00:00
prev := ""
2022-04-17 20:57:12 +00:00
for i, value := range v.(Array) {
2022-03-29 16:13:41 +00:00
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
2022-04-02 20:59:36 +00:00
if m.cursor < len(m.paths)-1 { // scroll till last element in m.paths
2022-03-29 16:13:41 +00:00
m.cursor++
} else {
// at the bottom of viewport maybe some hidden brackets, lets scroll to see them
if !m.AtBottom() {
m.LineDown(1)
}
}
2022-04-02 20:59:36 +00:00
if m.cursor >= len(m.paths) {
m.cursor = len(m.paths) - 1
2022-03-29 16:13:41 +00:00
}
}
func (m *model) up() {
m.showCursor = true
if m.cursor > 0 {
m.cursor--
}
2022-04-02 20:59:36 +00:00
if m.cursor >= len(m.paths) {
m.cursor = len(m.paths) - 1
2022-03-29 16:13:41 +00:00
}
}