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

1189 lines
24 KiB
Go

package main
import (
8 months ago
"encoding/json"
8 months ago
"errors"
"flag"
"fmt"
"io"
8 months ago
"io/fs"
"math"
"os"
8 months ago
"path"
8 months ago
"regexp"
"runtime/pprof"
8 months ago
"strconv"
8 months ago
"strings"
8 months ago
8 months ago
"github.com/antonmedv/clipboard"
8 months ago
"github.com/charmbracelet/bubbles/key"
8 months ago
"github.com/charmbracelet/bubbles/textinput"
2 months ago
"github.com/charmbracelet/bubbles/viewport"
8 months ago
tea "github.com/charmbracelet/bubbletea"
8 months ago
"github.com/charmbracelet/lipgloss"
"github.com/goccy/go-yaml"
8 months ago
"github.com/mattn/go-isatty"
"github.com/sahilm/fuzzy"
8 months ago
"github.com/antonmedv/fx/internal/complete"
. "github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/theme"
8 months ago
jsonpath "github.com/antonmedv/fx/path"
)
8 months ago
var (
flagYaml bool
flagComp bool
8 months ago
)
func main() {
8 months ago
if _, ok := os.LookupEnv("FX_PPROF"); ok {
f, err := os.Create("cpu.prof")
if err != nil {
panic(err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
panic(err)
}
defer f.Close()
8 months ago
defer pprof.StopCPUProfile()
memProf, err := os.Create("mem.prof")
if err != nil {
panic(err)
}
defer memProf.Close()
8 months ago
defer pprof.WriteHeapProfile(memProf)
9 months ago
}
if complete.Complete() {
os.Exit(0)
return
}
2 months ago
8 months ago
var args []string
for _, arg := range os.Args[1:] {
if strings.HasPrefix(arg, "--comp") {
flagComp = true
continue
}
8 months ago
switch arg {
case "-h", "--help":
fmt.Println(usage(keyMap))
return
8 months ago
case "-v", "-V", "--version":
fmt.Println(version)
return
8 months ago
case "--themes":
theme.ThemeTester()
8 months ago
return
case "--export-themes":
theme.ExportThemes()
return
8 months ago
default:
args = append(args, arg)
}
}
8 months ago
if flagComp {
shell := flag.String("comp", "", "")
flag.Parse()
switch *shell {
case "bash":
fmt.Print(complete.Bash())
case "zsh":
fmt.Print(complete.Zsh())
2 months ago
case "fish":
fmt.Print(complete.Fish())
default:
fmt.Println("unknown shell type")
}
8 months ago
return
}
8 months ago
stdinIsTty := isatty.IsTerminal(os.Stdin.Fd())
var fileName string
var src io.Reader
if stdinIsTty && len(args) == 0 {
fmt.Println(usage(keyMap))
return
} else if stdinIsTty && len(args) == 1 {
8 months ago
filePath := args[0]
f, err := os.Open(filePath)
if err != nil {
8 months ago
var pathError *fs.PathError
if errors.As(err, &pathError) {
8 months ago
fmt.Println(err)
os.Exit(1)
8 months ago
} else {
8 months ago
panic(err)
}
}
fileName = path.Base(filePath)
src = f
hasYamlExt, _ := regexp.MatchString(`(?i)\.ya?ml$`, fileName)
if !flagYaml && hasYamlExt {
flagYaml = true
}
8 months ago
} else if !stdinIsTty && len(args) == 0 {
8 months ago
src = os.Stdin
8 months ago
} else {
reduce(os.Args[1:])
8 months ago
return
8 months ago
}
data, err := io.ReadAll(src)
if err != nil {
panic(err)
}
if flagYaml {
data, err = yaml.YAMLToJSON(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
return
}
}
head, err := Parse(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
return
}
8 months ago
digInput := textinput.New()
digInput.Prompt = ""
digInput.TextStyle = lipgloss.NewStyle().
Background(lipgloss.Color("7")).
Foreground(lipgloss.Color("0"))
digInput.Cursor.Style = lipgloss.NewStyle().
Background(lipgloss.Color("15")).
Foreground(lipgloss.Color("0"))
8 months ago
searchInput := textinput.New()
searchInput.Prompt = "/"
m := &model{
8 months ago
head: head,
top: head,
showCursor: true,
wrap: true,
fileName: fileName,
digInput: digInput,
searchInput: searchInput,
8 months ago
search: newSearch(),
}
lipgloss.SetColorProfile(theme.TermOutput.ColorProfile())
2 months ago
2 months ago
withMouse := tea.WithMouseCellMotion()
if _, ok := os.LookupEnv("FX_NO_MOUSE"); ok {
withMouse = tea.WithAltScreen()
}
2 months ago
p := tea.NewProgram(m,
tea.WithAltScreen(),
2 months ago
withMouse,
2 months ago
tea.WithOutput(os.Stderr),
)
_, err = p.Run()
if err != nil {
panic(err)
}
2 months ago
if m.printOnExit {
fmt.Println(m.cursorValue())
}
}
type model struct {
8 months ago
termWidth, termHeight int
head, top *Node
8 months ago
cursor int // cursor position [0, termHeight)
8 months ago
showCursor bool
8 months ago
wrap bool
8 months ago
margin int
8 months ago
fileName string
8 months ago
digInput textinput.Model
8 months ago
searchInput textinput.Model
8 months ago
search *search
8 months ago
yank bool
2 months ago
showHelp bool
help viewport.Model
2 months ago
showPreview bool
preview viewport.Model
2 months ago
printOnExit bool
}
func (m *model) Init() tea.Cmd {
return nil
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
2 months ago
if msg, ok := msg.(tea.WindowSizeMsg); ok {
8 months ago
m.termWidth = msg.Width
m.termHeight = msg.Height
2 months ago
m.help.Width = m.termWidth
m.help.Height = m.termHeight - 1
2 months ago
m.preview.Width = m.termWidth
m.preview.Height = m.termHeight - 1
WrapAll(m.top, m.termWidth)
8 months ago
m.redoSearch()
2 months ago
}
if m.showHelp {
return m.handleHelpKey(msg)
}
2 months ago
if m.showPreview {
return m.handlePreviewKey(msg)
}
2 months ago
switch msg := msg.(type) {
case tea.MouseMsg:
switch msg.Type {
case tea.MouseWheelUp:
8 months ago
m.up()
case tea.MouseWheelDown:
8 months ago
m.down()
8 months ago
case tea.MouseLeft:
8 months ago
m.digInput.Blur()
8 months ago
m.showCursor = true
8 months ago
if msg.Y < m.viewHeight() {
8 months ago
if m.cursor == msg.Y {
to := m.cursorPointsTo()
if to != nil {
if to.IsCollapsed() {
to.Expand()
8 months ago
} else {
to.Collapse()
8 months ago
}
}
8 months ago
} else {
8 months ago
to := m.at(msg.Y)
if to != nil {
m.cursor = msg.Y
if to.IsCollapsed() {
to.Expand()
8 months ago
}
}
8 months ago
}
}
}
case tea.KeyMsg:
8 months ago
if m.digInput.Focused() {
return m.handleDigKey(msg)
}
8 months ago
if m.searchInput.Focused() {
return m.handleSearchKey(msg)
}
8 months ago
if m.yank {
return m.handleYankKey(msg)
}
return m.handleKey(msg)
}
return m, nil
}
8 months ago
func (m *model) handleDigKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch {
case key.Matches(msg, arrowUp):
m.up()
m.digInput.SetValue(m.cursorPath())
m.digInput.CursorEnd()
case key.Matches(msg, arrowDown):
m.down()
m.digInput.SetValue(m.cursorPath())
m.digInput.CursorEnd()
case msg.Type == tea.KeyEscape:
m.digInput.Blur()
case msg.Type == tea.KeyTab:
m.digInput.SetValue(m.cursorPath())
m.digInput.CursorEnd()
case msg.Type == tea.KeyEnter:
8 months ago
m.digInput.Blur()
digPath, ok := jsonpath.Split(m.digInput.Value())
if ok {
n := m.selectByPath(digPath)
if n != nil {
m.selectNode(n)
}
}
8 months ago
case key.Matches(msg, key.NewBinding(key.WithKeys("ctrl+w"))):
digPath, ok := jsonpath.Split(m.digInput.Value())
if ok {
if len(digPath) > 0 {
digPath = digPath[:len(digPath)-1]
}
n := m.selectByPath(digPath)
if n != nil {
m.selectNode(n)
m.digInput.SetValue(m.cursorPath())
m.digInput.CursorEnd()
}
}
case key.Matches(msg, textinput.DefaultKeyMap.WordBackward):
value := m.digInput.Value()
pth, ok := jsonpath.Split(value[0:m.digInput.Position()])
if ok {
if len(pth) > 0 {
pth = pth[:len(pth)-1]
m.digInput.SetCursor(len(jsonpath.Join(pth)))
} else {
m.digInput.CursorStart()
}
}
case key.Matches(msg, textinput.DefaultKeyMap.WordForward):
value := m.digInput.Value()
fullPath, ok1 := jsonpath.Split(value)
pth, ok2 := jsonpath.Split(value[0:m.digInput.Position()])
if ok1 && ok2 {
if len(pth) < len(fullPath) {
pth = append(pth, fullPath[len(pth)])
m.digInput.SetCursor(len(jsonpath.Join(pth)))
} else {
m.digInput.CursorEnd()
}
}
8 months ago
default:
if key.Matches(msg, key.NewBinding(key.WithKeys("."))) {
if m.digInput.Position() == len(m.digInput.Value()) {
m.digInput.SetValue(m.cursorPath())
m.digInput.CursorEnd()
}
}
8 months ago
m.digInput, cmd = m.digInput.Update(msg)
n := m.dig(m.digInput.Value())
if n != nil {
m.selectNode(n)
}
}
return m, cmd
}
2 months ago
func (m *model) handleHelpKey(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
if msg, ok := msg.(tea.KeyMsg); ok {
switch {
case key.Matches(msg, keyMap.Quit), key.Matches(msg, keyMap.Help):
2 months ago
m.showHelp = false
}
}
m.help, cmd = m.help.Update(msg)
return m, cmd
}
2 months ago
func (m *model) handlePreviewKey(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
if msg, ok := msg.(tea.KeyMsg); ok {
switch {
case key.Matches(msg, keyMap.Quit),
key.Matches(msg, keyMap.Preview):
2 months ago
m.showPreview = false
2 months ago
case key.Matches(msg, keyMap.Print):
return m, m.print()
2 months ago
}
}
m.preview, cmd = m.preview.Update(msg)
return m, cmd
}
8 months ago
func (m *model) handleSearchKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch {
case msg.Type == tea.KeyEscape:
m.searchInput.Blur()
8 months ago
m.searchInput.SetValue("")
8 months ago
m.doSearch("")
m.showCursor = true
8 months ago
case msg.Type == tea.KeyEnter:
m.searchInput.Blur()
8 months ago
m.doSearch(m.searchInput.Value())
8 months ago
default:
m.searchInput, cmd = m.searchInput.Update(msg)
}
return m, cmd
}
8 months ago
func (m *model) handleYankKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch {
8 months ago
case key.Matches(msg, yankPath):
8 months ago
_ = clipboard.WriteAll(m.cursorPath())
8 months ago
case key.Matches(msg, yankKey):
_ = clipboard.WriteAll(m.cursorKey())
2 months ago
case key.Matches(msg, yankValueY, yankValueV):
8 months ago
_ = clipboard.WriteAll(m.cursorValue())
}
m.yank = false
return m, nil
}
func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, keyMap.Quit):
return m, tea.Quit
2 months ago
case key.Matches(msg, keyMap.Help):
2 months ago
m.help.SetContent(help(keyMap))
2 months ago
m.showHelp = true
case key.Matches(msg, keyMap.Up):
8 months ago
m.up()
case key.Matches(msg, keyMap.Down):
8 months ago
m.down()
case key.Matches(msg, keyMap.PageUp):
m.cursor = 0
for i := 0; i < m.viewHeight(); i++ {
m.up()
}
case key.Matches(msg, keyMap.PageDown):
m.cursor = m.viewHeight() - 1
for i := 0; i < m.viewHeight(); i++ {
m.down()
}
m.scrollIntoView()
8 months ago
case key.Matches(msg, keyMap.HalfPageUp):
m.cursor = 0
for i := 0; i < m.viewHeight()/2; i++ {
m.up()
}
case key.Matches(msg, keyMap.HalfPageDown):
m.cursor = m.viewHeight() - 1
for i := 0; i < m.viewHeight()/2; i++ {
m.down()
8 months ago
}
m.scrollIntoView()
8 months ago
case key.Matches(msg, keyMap.GotoTop):
m.head = m.top
8 months ago
m.cursor = 0
8 months ago
m.showCursor = true
8 months ago
case key.Matches(msg, keyMap.GotoBottom):
m.head = m.findBottom()
8 months ago
m.cursor = 0
8 months ago
m.showCursor = true
8 months ago
m.scrollIntoView()
8 months ago
case key.Matches(msg, keyMap.NextSibling):
pointsTo := m.cursorPointsTo()
var nextSibling *Node
if pointsTo.End != nil && pointsTo.End.Next != nil {
nextSibling = pointsTo.End.Next
8 months ago
} else {
nextSibling = pointsTo.Next
8 months ago
}
if nextSibling != nil {
8 months ago
m.selectNode(nextSibling)
8 months ago
}
case key.Matches(msg, keyMap.PrevSibling):
8 months ago
pointsTo := m.cursorPointsTo()
var prevSibling *Node
if pointsTo.Parent() != nil && pointsTo.Parent().End == pointsTo {
prevSibling = pointsTo.Parent()
} else if pointsTo.Prev != nil {
prevSibling = pointsTo.Prev
parent := prevSibling.Parent()
if parent != nil && parent.End == prevSibling {
8 months ago
prevSibling = parent
}
}
if prevSibling != nil {
8 months ago
m.selectNode(prevSibling)
8 months ago
}
8 months ago
8 months ago
case key.Matches(msg, keyMap.Collapse):
8 months ago
n := m.cursorPointsTo()
if n.HasChildren() && !n.IsCollapsed() {
n.Collapse()
8 months ago
} else {
if n.Parent() != nil {
n = n.Parent()
8 months ago
}
8 months ago
}
8 months ago
m.selectNode(n)
8 months ago
case key.Matches(msg, keyMap.Expand):
m.cursorPointsTo().Expand()
8 months ago
m.showCursor = true
8 months ago
8 months ago
case key.Matches(msg, keyMap.CollapseRecursively):
n := m.cursorPointsTo()
if n.HasChildren() {
n.CollapseRecursively()
8 months ago
}
8 months ago
m.showCursor = true
8 months ago
case key.Matches(msg, keyMap.ExpandRecursively):
n := m.cursorPointsTo()
if n.HasChildren() {
n.ExpandRecursively(0, math.MaxInt)
8 months ago
}
8 months ago
m.showCursor = true
8 months ago
case key.Matches(msg, keyMap.CollapseAll):
n := m.top
for n != nil {
n.CollapseRecursively()
if n.End == nil {
n = nil
} else {
n = n.End.Next
}
}
8 months ago
m.cursor = 0
m.head = m.top
8 months ago
m.showCursor = true
8 months ago
case key.Matches(msg, keyMap.ExpandAll):
at := m.cursorPointsTo()
n := m.top
for n != nil {
n.ExpandRecursively(0, math.MaxInt)
if n.End == nil {
n = nil
} else {
n = n.End.Next
}
}
8 months ago
m.selectNode(at)
case key.Matches(msg, keyMap.CollapseLevel):
at := m.cursorPointsTo()
if at != nil && at.HasChildren() {
toLevel, _ := strconv.Atoi(msg.String())
at.CollapseRecursively()
at.ExpandRecursively(0, toLevel)
m.showCursor = true
}
8 months ago
case key.Matches(msg, keyMap.ToggleWrap):
at := m.cursorPointsTo()
m.wrap = !m.wrap
if m.wrap {
WrapAll(m.top, m.termWidth)
8 months ago
} else {
DropWrapAll(m.top)
8 months ago
}
if at.Chunk != nil && at.Value == nil {
at = at.Parent()
8 months ago
}
8 months ago
m.redoSearch()
8 months ago
m.selectNode(at)
8 months ago
case key.Matches(msg, keyMap.Yank):
m.yank = true
2 months ago
case key.Matches(msg, keyMap.Preview):
m.showPreview = true
content := lipgloss.NewStyle().Width(m.termWidth).Render(m.cursorValue())
m.preview.SetContent(content)
m.preview.GotoTop()
2 months ago
2 months ago
case key.Matches(msg, keyMap.Print):
return m, m.print()
8 months ago
case key.Matches(msg, keyMap.Dig):
m.digInput.SetValue(m.cursorPath() + ".")
8 months ago
m.digInput.CursorEnd()
m.digInput.Width = m.termWidth - 1
m.digInput.Focus()
8 months ago
case key.Matches(msg, keyMap.Search):
m.searchInput.CursorEnd()
m.searchInput.Width = m.termWidth - 2 // -1 for the prompt, -1 for the cursor
m.searchInput.Focus()
case key.Matches(msg, keyMap.SearchNext):
8 months ago
m.selectSearchResult(m.search.cursor + 1)
8 months ago
case key.Matches(msg, keyMap.SearchPrev):
8 months ago
m.selectSearchResult(m.search.cursor - 1)
8 months ago
}
return m, nil
}
8 months ago
func (m *model) up() {
8 months ago
m.showCursor = true
8 months ago
m.cursor--
if m.cursor < 0 {
m.cursor = 0
if m.head.Prev != nil {
m.head = m.head.Prev
8 months ago
}
}
}
func (m *model) down() {
8 months ago
m.showCursor = true
8 months ago
m.cursor++
n := m.cursorPointsTo()
if n == nil {
m.cursor--
return
}
if m.cursor >= m.viewHeight() {
m.cursor = m.viewHeight() - 1
if m.head.Next != nil {
m.head = m.head.Next
8 months ago
}
}
}
func (m *model) visibleLines() int {
visibleLines := 0
n := m.head
for n != nil && visibleLines < m.viewHeight() {
visibleLines++
n = n.Next
8 months ago
}
return visibleLines
}
8 months ago
func (m *model) scrollIntoView() {
8 months ago
visibleLines := m.visibleLines()
8 months ago
if m.cursor >= visibleLines {
m.cursor = visibleLines - 1
}
for visibleLines < m.viewHeight() && m.head.Prev != nil {
8 months ago
visibleLines++
m.cursor++
m.head = m.head.Prev
8 months ago
}
}
func (m *model) View() string {
2 months ago
if m.showHelp {
statusBar := flex(m.termWidth, ": press q or ? to close help", "")
return m.help.View() + "\n" + string(theme.CurrentTheme.StatusBar([]byte(statusBar)))
2 months ago
}
2 months ago
if m.showPreview {
statusBar := flex(m.termWidth, m.cursorPath(), m.fileName)
return m.preview.View() + "\n" + string(theme.CurrentTheme.StatusBar([]byte(statusBar)))
2 months ago
}
var screen []byte
8 months ago
n := m.head
8 months ago
8 months ago
printedLines := 0
8 months ago
for lineNumber := 0; lineNumber < m.viewHeight(); lineNumber++ {
8 months ago
if n == nil {
break
}
for ident := 0; ident < int(n.Depth); ident++ {
screen = append(screen, ' ', ' ')
}
8 months ago
8 months ago
isSelected := m.cursor == lineNumber
if !m.showCursor {
isSelected = false // don't highlight the cursor while iterating search results
}
8 months ago
if n.Key != nil {
8 months ago
screen = append(screen, m.prettyKey(n, isSelected)...)
screen = append(screen, theme.Colon...)
8 months ago
isSelected = false // don't highlight the key's value
}
8 months ago
8 months ago
screen = append(screen, m.prettyPrint(n, isSelected)...)
8 months ago
if n.IsCollapsed() {
if n.Value[0] == '{' {
if n.Collapsed.Key != nil {
screen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Key)...)
screen = append(screen, theme.ColonPreview...)
8 months ago
}
screen = append(screen, theme.Dot3...)
screen = append(screen, theme.CloseCurlyBracket...)
} else if n.Value[0] == '[' {
screen = append(screen, theme.Dot3...)
screen = append(screen, theme.CloseSquareBracket...)
8 months ago
}
if n.End != nil && n.End.Comma {
screen = append(screen, theme.Comma...)
}
8 months ago
}
if n.Comma {
screen = append(screen, theme.Comma...)
}
8 months ago
if theme.ShowSizes && len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') {
if n.IsCollapsed() || n.Size > 1 {
screen = append(screen, theme.CurrentTheme.Size([]byte(fmt.Sprintf(" // %d", n.Size)))...)
}
}
screen = append(screen, '\n')
8 months ago
printedLines++
n = n.Next
}
8 months ago
8 months ago
for i := printedLines; i < m.viewHeight(); i++ {
screen = append(screen, theme.Empty...)
8 months ago
screen = append(screen, '\n')
}
8 months ago
8 months ago
if m.digInput.Focused() {
screen = append(screen, m.digInput.View()...)
} else {
8 months ago
statusBar := flex(m.termWidth, m.cursorPath(), m.fileName)
screen = append(screen, theme.CurrentTheme.StatusBar([]byte(statusBar))...)
8 months ago
}
8 months ago
8 months ago
if m.yank {
screen = append(screen, '\n')
screen = append(screen, []byte("(y)value (p)path (k)key")...)
} else if m.searchInput.Focused() {
8 months ago
screen = append(screen, '\n')
screen = append(screen, m.searchInput.View()...)
} else if m.searchInput.Value() != "" {
screen = append(screen, '\n')
re, ci := regexCase(m.searchInput.Value())
8 months ago
re = "/" + re + "/"
8 months ago
if ci {
8 months ago
re += "i"
8 months ago
}
8 months ago
if m.search.err != nil {
screen = append(screen, flex(m.termWidth, re, m.search.err.Error())...)
} else if len(m.search.results) == 0 {
8 months ago
screen = append(screen, flex(m.termWidth, re, "not found")...)
} else {
8 months ago
cursor := fmt.Sprintf("found: [%v/%v]", m.search.cursor+1, len(m.search.results))
8 months ago
screen = append(screen, flex(m.termWidth, re, cursor)...)
}
}
return string(screen)
}
8 months ago
func (m *model) prettyKey(node *Node, selected bool) []byte {
b := node.Key
8 months ago
style := theme.CurrentTheme.Key
8 months ago
if selected {
style = theme.CurrentTheme.Cursor
8 months ago
}
if indexes, ok := m.search.keys[node]; ok {
var out []byte
for i, p := range splitBytesByIndexes(b, indexes) {
8 months ago
if i%2 == 0 {
out = append(out, style(p.b)...)
} else if p.index == m.search.cursor {
out = append(out, theme.CurrentTheme.Cursor(p.b)...)
8 months ago
} else {
out = append(out, theme.CurrentTheme.Search(p.b)...)
8 months ago
}
}
return out
} else {
return style(b)
}
}
func (m *model) prettyPrint(node *Node, selected bool) []byte {
8 months ago
var b []byte
if node.Chunk != nil {
b = node.Chunk
8 months ago
} else {
b = node.Value
8 months ago
}
if len(b) == 0 {
return b
}
style := theme.Value(b, selected, node.Chunk != nil)
8 months ago
8 months ago
if indexes, ok := m.search.values[node]; ok {
8 months ago
var out []byte
for i, p := range splitBytesByIndexes(b, indexes) {
8 months ago
if i%2 == 0 {
out = append(out, style(p.b)...)
} else if p.index == m.search.cursor {
out = append(out, theme.CurrentTheme.Cursor(p.b)...)
8 months ago
} else {
out = append(out, theme.CurrentTheme.Search(p.b)...)
8 months ago
}
}
return out
} else {
return style(b)
}
}
8 months ago
func (m *model) viewHeight() int {
8 months ago
if m.searchInput.Focused() || m.searchInput.Value() != "" {
return m.termHeight - 2
}
8 months ago
if m.yank {
return m.termHeight - 2
}
8 months ago
return m.termHeight - 1
8 months ago
}
func (m *model) cursorPointsTo() *Node {
8 months ago
return m.at(m.cursor)
}
func (m *model) at(pos int) *Node {
8 months ago
head := m.head
8 months ago
for i := 0; i < pos; i++ {
8 months ago
if head == nil {
8 months ago
break
8 months ago
}
head = head.Next
8 months ago
}
return head
}
func (m *model) findBottom() *Node {
8 months ago
n := m.head
for n.Next != nil {
if n.End != nil {
n = n.End
8 months ago
} else {
n = n.Next
8 months ago
}
}
return n
8 months ago
}
8 months ago
func (m *model) nodeInsideView(n *Node) bool {
8 months ago
if n == nil {
return false
}
head := m.head
for i := 0; i < m.viewHeight(); i++ {
if head == nil {
break
}
if head == n {
return true
}
head = head.Next
8 months ago
}
return false
}
func (m *model) selectNodeInView(n *Node) {
8 months ago
head := m.head
for i := 0; i < m.viewHeight(); i++ {
if head == nil {
break
}
if head == n {
m.cursor = i
return
}
head = head.Next
8 months ago
}
}
8 months ago
func (m *model) selectNode(n *Node) {
8 months ago
m.showCursor = true
8 months ago
if m.nodeInsideView(n) {
m.selectNodeInView(n)
m.scrollIntoView()
} else {
m.cursor = 0
m.head = n
m.scrollIntoView()
}
parent := n.Parent()
8 months ago
for parent != nil {
parent.Expand()
parent = parent.Parent()
8 months ago
}
8 months ago
}
8 months ago
func (m *model) cursorPath() string {
path := ""
at := m.cursorPointsTo()
for at != nil {
if at.Prev != nil {
if at.Chunk != nil && at.Value == nil {
at = at.Parent()
8 months ago
}
if at.Key != nil {
quoted := string(at.Key)
8 months ago
unquoted, err := strconv.Unquote(quoted)
if err == nil && jsonpath.Identifier.MatchString(unquoted) {
8 months ago
path = "." + unquoted + path
} else {
path = "[" + quoted + "]" + path
}
} else if at.Index >= 0 {
path = "[" + strconv.Itoa(at.Index) + "]" + path
8 months ago
}
}
at = at.Parent()
8 months ago
}
return path
}
8 months ago
8 months ago
func (m *model) cursorValue() string {
at := m.cursorPointsTo()
if at == nil {
return ""
}
parent := at.Parent()
2 months ago
if parent != nil {
// wrapped string part
if at.Chunk != nil && at.Value == nil {
2 months ago
at = parent
}
if len(at.Value) == 1 && at.Value[0] == '}' || at.Value[0] == ']' {
2 months ago
at = parent
}
}
if len(at.Value) > 0 && at.Value[0] == '"' {
str, err := strconv.Unquote(string(at.Value))
if err == nil {
return str
}
return string(at.Value)
8 months ago
}
var out strings.Builder
out.Write(at.Value)
out.WriteString("\n")
if at.HasChildren() {
it := at.Next
if at.IsCollapsed() {
it = at.Collapsed
8 months ago
}
for it != nil {
out.WriteString(strings.Repeat(" ", int(it.Depth-at.Depth)))
if it.Key != nil {
out.Write(it.Key)
8 months ago
out.WriteString(": ")
}
if it.Value != nil {
out.Write(it.Value)
8 months ago
}
if it == at.End {
8 months ago
break
}
if it.Comma {
out.WriteString(",")
8 months ago
}
out.WriteString("\n")
if it.ChunkEnd != nil {
it = it.ChunkEnd.Next
} else if it.IsCollapsed() {
it = it.Collapsed
8 months ago
} else {
it = it.Next
8 months ago
}
}
}
return out.String()
}
8 months ago
func (m *model) cursorKey() string {
at := m.cursorPointsTo()
if at == nil {
return ""
}
2 months ago
if at.IsWrap() {
at = at.Parent()
}
if at.Key != nil {
8 months ago
var v string
_ = json.Unmarshal(at.Key, &v)
8 months ago
return v
}
return strconv.Itoa(at.Index)
8 months ago
}
func (m *model) selectByPath(path []any) *Node {
n := m.currentTopNode()
for _, part := range path {
8 months ago
if n == nil {
return nil
}
switch part := part.(type) {
case string:
n = n.FindChildByKey(part)
8 months ago
case int:
n = n.FindChildByIndex(part)
8 months ago
}
}
return n
}
8 months ago
func (m *model) currentTopNode() *Node {
at := m.cursorPointsTo()
if at == nil {
return nil
}
for at.Parent() != nil {
at = at.Parent()
}
return at
}
8 months ago
func (m *model) doSearch(s string) {
8 months ago
m.search = newSearch()
8 months ago
8 months ago
if s == "" {
return
}
8 months ago
code, ci := regexCase(s)
if ci {
code = "(?i)" + code
}
re, err := regexp.Compile(code)
if err != nil {
8 months ago
m.search.err = err
8 months ago
return
}
8 months ago
n := m.top
8 months ago
searchIndex := 0
8 months ago
for n != nil {
if n.Key != nil {
indexes := re.FindAllIndex(n.Key, -1)
8 months ago
if len(indexes) > 0 {
for i, pair := range indexes {
m.search.results = append(m.search.results, n)
m.search.keys[n] = append(m.search.keys[n], match{start: pair[0], end: pair[1], index: searchIndex + i})
}
searchIndex += len(indexes)
}
}
indexes := re.FindAllIndex(n.Value, -1)
8 months ago
if len(indexes) > 0 {
for range indexes {
8 months ago
m.search.results = append(m.search.results, n)
8 months ago
}
if n.Chunk != nil {
8 months ago
// String can be split into chunks, so we need to map the indexes to the chunks.
chunks := [][]byte{n.Chunk}
chunkNodes := []*Node{n}
8 months ago
it := n.Next
8 months ago
for it != nil {
chunkNodes = append(chunkNodes, it)
chunks = append(chunks, it.Chunk)
if it == n.ChunkEnd {
8 months ago
break
}
it = it.Next
8 months ago
}
8 months ago
chunkMatches := splitIndexesToChunks(chunks, indexes, searchIndex)
8 months ago
for i, matches := range chunkMatches {
8 months ago
m.search.values[chunkNodes[i]] = matches
8 months ago
}
} else {
8 months ago
for i, pair := range indexes {
8 months ago
m.search.values[n] = append(m.search.values[n], match{start: pair[0], end: pair[1], index: searchIndex + i})
8 months ago
}
8 months ago
}
8 months ago
searchIndex += len(indexes)
8 months ago
}
if n.IsCollapsed() {
n = n.Collapsed
8 months ago
} else {
n = n.Next
8 months ago
}
}
m.selectSearchResult(0)
}
func (m *model) selectSearchResult(i int) {
8 months ago
if len(m.search.results) == 0 {
8 months ago
return
}
if i < 0 {
8 months ago
i = len(m.search.results) - 1
8 months ago
}
8 months ago
if i >= len(m.search.results) {
8 months ago
i = 0
}
8 months ago
m.search.cursor = i
result := m.search.results[i]
8 months ago
m.selectNode(result)
8 months ago
m.showCursor = false
}
func (m *model) redoSearch() {
if m.searchInput.Value() != "" && len(m.search.results) > 0 {
cursor := m.search.cursor
m.doSearch(m.searchInput.Value())
m.selectSearchResult(cursor)
}
8 months ago
}
func (m *model) dig(v string) *Node {
p, ok := jsonpath.Split(v)
if !ok {
return nil
}
at := m.selectByPath(p)
if at != nil {
return at
}
lastPart := p[len(p)-1]
searchTerm, ok := lastPart.(string)
if !ok {
return nil
}
p = p[:len(p)-1]
at = m.selectByPath(p)
if at == nil {
return nil
}
keys, nodes := at.Children()
matches := fuzzy.Find(searchTerm, keys)
if len(matches) == 0 {
return nil
}
return nodes[matches[0].Index]
}
2 months ago
func (m *model) print() tea.Cmd {
m.printOnExit = true
return tea.Quit
}